HotSchedules Integration REST API Guide

This guide is for developers who want to connect their own systems to HotSchedules: an HR system, a payroll or time-keeping tool, a POS, a data warehouse. It explains what the Integration REST API does, how to sign in, how to read and write data, and what each call is for. You do not need to know HotSchedules or the older SOAP API to use it.

The guide has three chapters:

Links on this page contain no host name, so they open the Swagger of the environment you are reading this page on. In the curl examples, the base URL is the address this page is served from.

Getting started

How HotSchedules is organized

HotSchedules describes a business as a tree, and almost every call is addressed by a position in that tree.

Around the stores, HotSchedules keeps the data a workforce system needs:

The URLs use identifiers you can read from the API itself:

In the URL What it is Where to read it
conceptId The concept's external reference, extId GET /api/v1/concepts
groupId The group's external reference, extId GET /api/v1/concepts/{conceptId}/groups
storeId The store's alphanumeric store ID, alphanumericStoreNum GET /api/v1/concepts/{conceptId}/stores

Always read these IDs from the API instead of assuming them. A store's numeric storeNum is not always the same as its alphanumeric ID.

Signing in

Every call except the health check needs credentials. Your Fourth contact provides them. There are two ways to send them.

With an API username and password

Send them as a standard HTTP Basic Authorization header, the Base64 encoding of username:password. curl does this for you:

curl -u "$API_USERNAME:$API_PASSWORD" "https://hs-8c6dbbdc6fdf4a49be54789f292ba043.ecs.us-east-1.on.aws/api/v1/concepts"

With an OAuth 2.0 access token

Request a token from Fourth's OAuth server with the client credentials grant, then send it as a bearer token on every call:

curl -H "Authorization: Bearer $ACCESS_TOKEN" "https://hs-8c6dbbdc6fdf4a49be54789f292ba043.ecs.us-east-1.on.aws/api/v1/concepts"

Ask your Fourth contact for the client ID, client secret, token URL and scope for each environment. A token that is expired or lacks the required scope is rejected with 401.

Which stores a credential can reach

A credential is either company-wide or limited to certain groups. A company-wide credential reaches every store in your company. A group-limited credential reaches only the stores in its groups, and a call under /api/v1/concepts/{conceptId} for any other store returns 403. The average wage call is the exception: it reports such a store as a failed row in its summary. The two company-level employee calls need a company-wide credential. OAuth tokens are always company-wide, so use an API username and password if you need to keep a system limited to some groups.

Making your first call

Start with the health check. It needs no credentials and tells you the service is reachable:

curl "https://hs-8c6dbbdc6fdf4a49be54789f292ba043.ecs.us-east-1.on.aws/api/v1/health"

Then walk down the tree. List your concepts, pick one, list its stores and read the alphanumericStoreNum of the store you want:

curl -u "$API_USERNAME:$API_PASSWORD" "https://hs-8c6dbbdc6fdf4a49be54789f292ba043.ecs.us-east-1.on.aws/api/v1/concepts"
curl -u "$API_USERNAME:$API_PASSWORD" "https://hs-8c6dbbdc6fdf4a49be54789f292ba043.ecs.us-east-1.on.aws/api/v1/concepts/7/stores"

With a concept and a store ID you can read the store's data, for example its active employees:

curl -u "$API_USERNAME:$API_PASSWORD" "https://hs-8c6dbbdc6fdf4a49be54789f292ba043.ecs.us-east-1.on.aws/api/v1/concepts/7/stores/42/employees?activeOnly=true"

If a call returns 403 with a message about a permission, the store has not been enabled for that call. See Permissions, rate limits and errors.

Conventions you can rely on

Permissions, rate limits and errors

Permissions. Most store-level calls need an API permission enabled on the store, for example 3009 Get Store Employees. Without it, the call returns 403 with the message <permission> not enabled for this store. A few calls also depend on a company setting. Ask Fourth support to enable what you need. The reference names the permission for every call.

Rate limits. Limits apply per company and per call. Fourth can change the limits for a company. Every response to a rate-limited call that is allowed through carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (seconds until the window resets). A call over the limit returns 429 with a Retry-After header. Wait that many seconds and retry.

Errors. Errors use standard HTTP statuses and a JSON body with timestamp, status, error, message and path. Two differ: a 401 has no body, and a 429 has its own body with error, message and retryAfter.

Status Typical cause
400 A missing or malformed parameter, or a value outside its allowed range
401 Bad or missing credentials. The response has no body.
403 A permission or setting is not enabled, or the credential cannot see the store
404 The concept, group or store was not found for your company
409 The request conflicts with the record's state, for example an overlapping time-off update
429 You hit the rate limit
501 The availability call, for a company whose business day ends on the calendar day

Common integration recipes

API reference

Each entry gives the call, what it does and when to use it, the store permission it needs, and the SOAP method it replaces with a link to the mapping. Calls that have no SOAP counterpart say so.

Company structure

These calls describe your hierarchy and the setup that hangs off it. They need no store permission. A group-limited credential sees only its own groups and stores.

List concepts

GET /api/v1/concepts returns the concepts of your company, each with id, extId and name. Use extId as conceptId in every later call. extId is -1 when a concept has no external reference.

SOAP counterpart: HierarchyService.getConcepts and getConceptsV2. See Concepts, groups and stores.

List groups

GET /api/v1/concepts/{conceptId}/groups returns the groups of a concept: extId, name and conceptExtId. Use extId as groupId.

SOAP counterpart: HierarchyService.getGroups. See Concepts, groups and stores.

List stores

GET /api/v1/concepts/{conceptId}/stores returns the stores of a concept with their name, address, time zone, whether they are active, and their group. Pass groupId to list one group only, or leave it out to list every store. Read alphanumericStoreNum from here and use it as storeId everywhere else.

SOAP counterpart: HierarchyService.getStores and getStoresV2. See Concepts, groups and stores.

List revenue centers

GET /api/v1/concepts/{conceptId}/groups/{groupId}/revenue-centers returns the revenue centers of a group. A revenue center is a part of the business that sales are reported against, for example a bar or a dining room. Each has extId, name and groupLevel, which says whether it belongs to the whole group or to one store.

SOAP counterpart: HierarchyService.getGroupRVCs. See Revenue centers, sales categories and group jobs.

Create or update a revenue center

PUT /api/v1/concepts/{conceptId}/groups/{groupId}/revenue-centers creates a revenue center when its ID is new and updates its name otherwise. Send rvcId, rvcName, groupLevel and, for a store-level entry, the storeId that owns it. The name cannot be blank or longer than 50 characters, and storeId is a number, not the alphanumeric store ID. A violation comes back as 200 with failCount: 1 in the summary, not as an HTTP error. The call needs the revenue center setter enabled for your company, otherwise 403.

SOAP counterpart: HierarchyService.setRVC. See Revenue centers, sales categories and group jobs.

List sales categories

GET /api/v1/concepts/{conceptId}/groups/{groupId}/sales-categories returns the sales categories of a group, such as Food or Beverage, with extId, name and groupLevel.

SOAP counterpart: HierarchyService.getGroupSalesCats. See Revenue centers, sales categories and group jobs.

Create or update a sales category

PUT /api/v1/concepts/{conceptId}/groups/{groupId}/sales-categories works like the revenue center call, with salesCatId and salesCatName, and reports a violation the same way. It needs the sales category setter enabled for your company, otherwise 403.

SOAP counterpart: HierarchyService.setSalesCat. See Revenue centers, sales categories and group jobs.

List group jobs

GET /api/v1/concepts/{conceptId}/groups/{groupId}/jobs returns the active jobs defined for a group, each with its POS ID, name and default rate. Use it to learn which jobs exist before you assign them to employees.

SOAP counterpart: HierarchyService.getGroupJobs. See Revenue centers, sales categories and group jobs.

Employees

Read a store's employees

GET /api/v1/concepts/{conceptId}/stores/{storeId}/employees returns the employees of a store with their profile, status, permission set, POS role, pay type and base pay, and custom fields. activeOnly leaves out terminated employees. excludeAscUsers leaves out above-store users. System and support users are never listed. Needs permission 3009 Get Store Employees.

SOAP counterpart: EmpService.getStoreEmployees through getStoreEmployeesV6. See Store employees.

Read employee info

GET /api/v1/concepts/{conceptId}/stores/{storeId}/employees/info returns a short record per employee: IDs, when the account was created and last updated, the permission set and the schedules the employee is assigned to. The timestamps help you find employees that changed. Needs permission 3020 Get Employee Info.

SOAP counterpart: EmpService.getEmpInfo and getEmpInfoV2. See Store employees.

Create and update a store's employees

PUT /api/v1/concepts/{conceptId}/stores/{storeId}/employees takes a list of employees. An employee that is not found by POS ID (or by HR ID, for companies that identify employees that way) is created, and one that is found is updated. Only the fields your company has enabled for integration are written, status is always applied, and an employee is never moved to another store. A salaried employee is described by payBaseAmount and a salaryFrequency. Needs permission 3007 Set Employees.

SOAP counterpart: EmpService.setEmps through setEmpsV4. See Store employees.

Read the company roster

GET /api/v1/employees returns one row per employee per store across the whole company, so an employee who works at three stores appears three times. Set aboveStore=true to include above-store employees and activeOnly=true to keep rows where the employee is active. It needs a company-wide credential and has a tight rate limit, so call it sparingly.

SOAP counterpart: EmpService.getCompanyEmployees. See Company and above-store employees.

Read above-store employees

GET /api/v1/employees/above-store returns the employees who work above the store level, for example managers over several stores. Each has a profile, a hierarchyNodes list with the path from the top of your hierarchy to the employee's place in it, and a securityGroup. It needs a company-wide credential.

SOAP counterpart: EmpService.getAboveStoreEmployees. See Company and above-store employees.

Jobs, wages and salaries

Read a store's jobs

GET /api/v1/concepts/{conceptId}/stores/{storeId}/jobs returns the active jobs of a store with POS ID, name and default rate. Needs permission 3010 Get Store Jobs.

SOAP counterpart: EmpService.getStoreJobs and getStoreJobsV2. See Jobs and wages.

Read employee jobs

GET /api/v1/concepts/{conceptId}/stores/{storeId}/employee-jobs returns which jobs each employee holds at the store, with regular and overtime wage, whether the job is primary, skill level and custom fields. Needs permission 3014 Get Emp Jobs.

SOAP counterpart: EmpService.getEmpJobs through getEmpJobsV6. See Jobs and wages.

Set employee jobs

PUT /api/v1/concepts/{conceptId}/stores/{storeId}/employee-jobs sets the jobs and wages of the employees in your list. Employees are matched by POS ID and jobs by job reference. For every employee in your list, jobs that are not in the list are removed, but only jobs that have an external reference. Needs permission 3008 Set Employee Jobs.

SOAP counterpart: EmpService.setEmpJobs through setEmpJobsV4. See Jobs and wages.

Read salaried jobs

GET /api/v1/concepts/{conceptId}/stores/{storeId}/salaried-jobs returns the salaried employees of the store with their primary job and annual salary in dollars. Needs permission 3014 Get Emp Jobs.

SOAP counterpart: EmpService.getSalariedJobs. See Jobs and wages.

Set salaried jobs

PUT /api/v1/concepts/{conceptId}/stores/{storeId}/salaried-jobs sets the annual salary of salaried employees. A missing, zero, negative or oversized salary fails that row. Needs permission 3008 Set Employee Jobs.

SOAP counterpart: EmpService.setSalariedJobs. See Jobs and wages.

Set average wages

PUT /api/v1/concepts/{conceptId}/average-wage sets the average hourly wage of many stores in one concept in a single call. Send the alphanumeric store ID and the wage for each. A store that is unknown, inactive or outside your credential's groups, or a negative wage, is reported as a failed row and does not stop the others. It needs alphanumeric store ID access enabled for your company, otherwise 403.

SOAP counterpart: EmpService.setAverageWage. See Jobs and wages.

Employee availability

Read employee availability

GET /api/v1/concepts/{conceptId}/stores/{storeId}/employees/availability returns, for each employee, the times they can and cannot work, as a list of calendars that each take effect on a date. Each calendar has weekly ranges of unavailable and available times and optional limits, such as the most hours per week or the fewest hours between shifts. availabilityType chooses which range lists to include. It never filters employees. Needs permission 3021 Get Employee Availability.

SOAP counterpart: EmpService.getEmpAvailability through getEmpAvailabilityV4. See Availability.

Labor positions and assignments

A labor position is a role in the store that an employee can be scheduled into, such as Line Cook. These calls need labor positions and position skill levels enabled for your company and store, otherwise 403.

Read labor positions

GET /api/v1/concepts/{conceptId}/stores/{storeId}/labor-positions returns the store's active labor positions that can be assigned to employees, with externalId and positionName.

SOAP counterpart: EmpService.getLaborPositionNames. See Labor positions.

Assign labor positions to employees

PUT /api/v1/concepts/{conceptId}/stores/{storeId}/employee-labor-positions sets which positions each employee holds. Find the employee by empHrId and each position by laborPositionExtId. The position name must match exactly or that assignment is rejected. Positions that are no longer listed are removed, and new ones start at the lowest enabled skill level. Needs permission 3051 Set Employee Labor Positions.

SOAP counterpart: EmpService.setEmpLaborPositions. See Labor positions.

Schedules

All schedule calls need permission 3004 Schedule Service. A schedule is a named set of shifts for a store, for example one per area of the restaurant.

Read shifts

GET /api/v1/concepts/{conceptId}/stores/{storeId}/schedules returns the shifts of a store that start in a window. Send startsAfter and startsBefore as ISO-8601 moments with an offset. The window can be at most 745 hours, which is enough to read a calendar month. Only scheduled, posted shifts are returned. Each shift has its schedule, location, start and end, regular and overtime duration, meal and break time, pay rates, job and employee. A shift with employee set to null is a house shift that nobody owns yet. Pass house to get only house shifts or only owned shifts.

SOAP counterpart: ScheduleService.getSchedule, getSchedule2, getScheduleV3 to getScheduleV5 and getShiftsV3. See Shifts.

Read schedule status

GET /api/v1/concepts/{conceptId}/stores/{storeId}/schedule-status says whether each schedule of a store is a draft or posted for a date, when it was posted and by whom, and whether it was edited after posting. statusDate defaults to today in the store's time zone. Use it to know whether a schedule has been posted. A schedule with no status for the date comes back as MISSING unless you pass omitMissing=true.

SOAP counterpart: ScheduleService.getScheduleStatus. See Schedule status.

Read applied templates

GET /api/v1/concepts/{conceptId}/stores/{storeId}/applied-templates returns the schedule templates managers have applied to upcoming weeks, with the shifts each template produced and their labor cost. weeksForward is required and runs from 1 to 5 whole weeks after the current one. Also needs permission 3022 Get Templates.

SOAP counterpart: ScheduleService.getAppliedTemplateFcstExport. See Applied templates.

Sales forecasts

Read projected sales

GET /api/v1/concepts/{conceptId}/stores/{storeId}/projected-sales returns the sales forecast of a store for each business date from start to end, both included. Each date has a total and the periods it is made of: day parts with a breakdown by sales category when the store forecasts by day part, otherwise the store's shifts. The service is read-only. Needs permission 3005 Projected Sales.

SOAP counterpart: ProjectedSalesService.getProjectedSales and getProjectedSalesV3. See Projected sales.

Time off

Time off is stored in two forms, shown in the source field of each record. DAY_PART is whole dates by day part. RANGE has a start and an end time. All time-off calls need permission 3050 Time Off Service.

Read approved time off as occurrences

GET /api/v1/concepts/{conceptId}/stores/{storeId}/time-off-occurrences returns a flat list with one item per stretch of approved time off in the date window start to end, both included. Time off that crosses midnight is split at midnight, so each day carries its own hours. It includes unpaid time off unless you pass paid_only=true. Each item has a timeOffId.

SOAP counterpart: TimeOffService.getApprovedPTO. See Reading approved time off.

Read approved time off as requests

GET /api/v1/concepts/{conceptId}/stores/{storeId}/time-off returns one record per approved request, each with a days list. It takes the same start, end and paid_only parameters.

SOAP counterpart: TimeOffService.getApprovedPTO, one item per request instead of per day. See Reading approved time off.

Create approved time off

POST /api/v1/concepts/{conceptId}/stores/{storeId}/time-off records approved time off for employees, each with posEmpId, an optional posJobId, typeCode, paid, timeOffMinutes, start and end. A bad item is reported by its index and does not stop the others. Creating time off unassigns the employee's overlapping scheduled shifts, and an item that overlaps existing time off for the employee is rejected. The store must use time ranges for time off and allow time-off requests.

SOAP counterpart: TimeOffService.setApprovedTimeOff and setApprovedTimeOffV2. See Creating approved time off.

Update approved time off

PATCH /api/v1/concepts/{conceptId}/stores/{storeId}/time-off/{timeOffId} changes the start, end and optionally the minutes and a reason for one record and returns the updated record. If the new range overlaps other time off for the employee, it returns 409 unless you pass override_overlap=true. It works only on approved records that have a start and end time.

No SOAP counterpart. See Updating and deleting approved time off.

Delete approved time off

DELETE /api/v1/concepts/{conceptId}/stores/{storeId}/time-off/{timeOffId} removes one record and returns 204. You can pass an optional reason. It disappears from reads and no longer counts as an overlap.

No SOAP counterpart. See Updating and deleting approved time off.

Sending POS checks

Send POS checks

POST /api/v1/concepts/{conceptId}/stores/{storeId}/sales accepts a list of closed POS checks for one store and returns 202 Accepted, which means the checks were accepted for processing. It is meant for sending each check as it closes. Every check needs employee, posRevenueCenter, openedAtUtc, itemCount, netTotal and grossTotal, and can carry totals, tenders, discounts, taxes and items. Every moment in a check is an ISO-8601 moment with an offset. A number or a moment without an offset returns 400. A group-limited credential gets 403 for a store outside its groups.

No SOAP counterpart. See POS check ingestion.

Checking the service is up

Check the service

GET /api/v1/health returns {"status":"ok"} and needs no credentials. Use it to test connectivity and to monitor the service.

SOAP counterpart: sayHello on every service. See Health check.

Migrating from the legacy SOAP API

This chapter is for teams moving an existing HotSchedules SOAP integration (EmpService, HierarchyService, ScheduleService, ProjectedSalesService, TimeOffService) to the Integration REST API. It covers how to authenticate, what changes in every call, and what each SOAP method turns into.

Every REST call below links to its entry in Swagger UI. For what each call does and when to use it, see the API reference.

At a glance

SOAP REST
XML envelope, one POST endpoint per service JSON, one URL per resource, standard HTTP verbs
WS-Security UsernameToken header HTTP Basic with the same API credentials, or an OAuth 2.0 bearer token
Several versions of the same method (getStoreEmployees to getStoreEmployeesV6) One endpoint per resource, always returning the full field set
Numeric store number in the request Alphanumeric store ID in the URL path
Dates without a time zone, salaries in cents, -1 for "none" ISO-8601 dates and timestamps with an offset, decimal amounts, null for "none"

Status by SOAP service:

SOAP service In REST?
HierarchyService Yes, every data method
EmpService Yes, except syncEmployeeRequiredFieldsUpdates
ScheduleService Yes, except getLocations and getTemplates, which are not carried over
ProjectedSalesService Yes
TimeOffService Yes, plus new update and delete calls
SalesService, TimeCardService, VolumeService, LaborService, CertificationService, OperatingBudgetPlanService, PunchRecordService Not yet. Keep using SOAP for these.
ProjectedLaborService Nothing to migrate. The service is deprecated and only sayHello is live.

Authentication

How SOAP authenticated

Each SOAP request carried your API username and password in a WS-Security UsernameToken header:

<soapenv:Header>
  <wsse:Security soapenv:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
    <wsse:UsernameToken>
      <wsse:Username>API_USERNAME</wsse:Username>
      <wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">API_PASSWORD</wsse:Password>
    </wsse:UsernameToken>
  </wsse:Security>
</soapenv:Header>

Option 1: HTTP Basic with your existing API credentials

The REST API accepts the same API username and password you use for SOAP. Send them as a standard Authorization: Basic header, which is the Base64 encoding of username:password. Do not send the two values separated by a space.

curl -u "$API_USERNAME:$API_PASSWORD" "https://hs-8c6dbbdc6fdf4a49be54789f292ba043.ecs.us-east-1.on.aws/api/v1/concepts"

This is the quickest way to migrate. You keep your credentials and your store access rules.

Option 2: OAuth 2.0 client credentials

Request an access token from Fourth's OAuth server with the client credentials grant, then send it on every call:

curl -H "Authorization: Bearer $ACCESS_TOKEN" "https://hs-8c6dbbdc6fdf4a49be54789f292ba043.ecs.us-east-1.on.aws/api/v1/concepts"

Fourth provisions the OAuth client for your company. Ask your Fourth contact for the client ID, client secret, token URL and scope for each environment. A token that is expired, has the wrong issuer or lacks the required scope is rejected with 401.

Which credentials see which stores

SOAP credentials can be company-wide ("master") or limited to certain groups. REST keeps the same rule:

Authentication errors

Status Meaning
401 The credentials or token are missing, malformed or wrong. The response has no body.
403 You are authenticated but not allowed: see Permissions and the group rule above.

What changes for every call

Addressing concepts, groups and stores

SOAP took the concept, group and store numbers as request elements. REST puts them in the URL path:

/api/v1/concepts/{conceptId}/stores/{storeId}/...

Because a store is identified by the URL, request items no longer repeat storeNum or clientId.

One endpoint replaces the versions

The numbered SOAP variants (V2, V3, ...) existed because each added fields or changed the store key. REST has no version parameter. Each endpoint returns the full field set, and a field that only some SOAP versions had is simply optional. The method sections below list which SOAP methods fold into which endpoint.

Dates and times

Money

SOAP salary amounts were integers in the smallest currency unit (cents for USD). REST uses decimal numbers in the store's main currency unit (dollars), on both read and write, for example 52000.00. Divide your old SOAP salary values by 100, or by the currency's scale. Hourly wages were already decimals and are unchanged.

Missing values

Where SOAP returned -1 or an empty string for "no value", REST returns null. Two exceptions keep a number: an employee with no POS mapping has posId 0 in schedule and time-off responses, and a job with no external reference has externalRef 0 in schedule responses.

Writes report one result per row

Calls that write a list (PUT .../employees, PUT .../employee-jobs and the other sync calls) return 200 with a summary, and a bad row does not stop the others:

{
  "successCount": 9,
  "failCount": 1,
  "errors": [ { "index": 3, "message": "Missing required field: firstName" } ]
}

index is the position of the row in your request. Sync calls are idempotent: sending a payload that already matches the stored data writes nothing, so retrying is safe.

Errors

Errors use standard HTTP statuses and a JSON body:

{
  "timestamp": "2026-09-19T14:02:11.482+00:00",
  "status": 404,
  "error": "Not Found",
  "message": "Store not found",
  "path": "/api/v1/concepts/7/stores/42/schedules"
}
Status Typical cause
400 A missing or malformed parameter, or a value outside its allowed range
401 Bad or missing credentials
403 A permission or setting is not enabled, or the credential cannot see the store
404 The concept, group or store was not found for your company
409 A time-off update or delete conflicts with the record's state
429 You hit the rate limit
501 Availability for a company whose business day ends on the calendar day

Permissions

Each store-level endpoint needs an API permission enabled on the store, the same permissions the SOAP services used (for example 3009 Get Store Employees). If it is missing, the call returns 403 with the message <permission> not enabled for this store. The method sections name the permission for each endpoint. Some endpoints also depend on a company setting. Ask Fourth support to enable these; the section for the endpoint says when one applies.

Rate limits

Limits apply per company and per endpoint. Fourth can raise or lower the limits for a company.

Every allowed response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (seconds until the window resets). A call over the limit returns 429 with a Retry-After header and its own body:

{
  "error": "rate_limit_exceeded",
  "message": "Rate limit exceeded. Limit: 10 requests per 1 seconds. Please retry after 1 seconds.",
  "retryAfter": 1
}

SOAP method index

Find your SOAP method, then follow the details link for the field mapping.

SOAP method REST call Details
HierarchyService.getConcepts, getConceptsV2 GET /api/v1/concepts Concepts, groups and stores
HierarchyService.getGroups GET /api/v1/concepts/{conceptId}/groups Concepts, groups and stores
HierarchyService.getStores, getStoresV2 GET /api/v1/concepts/{conceptId}/stores Concepts, groups and stores
HierarchyService.getGroupRVCs GET /api/v1/concepts/{conceptId}/groups/{groupId}/revenue-centers Revenue centers, sales categories and group jobs
HierarchyService.getGroupSalesCats GET /api/v1/concepts/{conceptId}/groups/{groupId}/sales-categories Revenue centers, sales categories and group jobs
HierarchyService.getGroupJobs GET /api/v1/concepts/{conceptId}/groups/{groupId}/jobs Revenue centers, sales categories and group jobs
HierarchyService.setRVC PUT /api/v1/concepts/{conceptId}/groups/{groupId}/revenue-centers Revenue centers, sales categories and group jobs
HierarchyService.setSalesCat PUT /api/v1/concepts/{conceptId}/groups/{groupId}/sales-categories Revenue centers, sales categories and group jobs
getStoreEmployees to getStoreEmployeesV6 GET /api/v1/concepts/{conceptId}/stores/{storeId}/employees Store employees
getEmpInfo, getEmpInfoV2 GET /api/v1/concepts/{conceptId}/stores/{storeId}/employees/info Store employees
setEmps to setEmpsV4 PUT /api/v1/concepts/{conceptId}/stores/{storeId}/employees Store employees
getCompanyEmployees GET /api/v1/employees Company and above-store employees
getAboveStoreEmployees GET /api/v1/employees/above-store Company and above-store employees
getStoreJobs, getStoreJobsV2 GET /api/v1/concepts/{conceptId}/stores/{storeId}/jobs Jobs and wages
getEmpJobs to getEmpJobsV6 GET /api/v1/concepts/{conceptId}/stores/{storeId}/employee-jobs Jobs and wages
setEmpJobs to setEmpJobsV4 PUT /api/v1/concepts/{conceptId}/stores/{storeId}/employee-jobs Jobs and wages
getSalariedJobs GET /api/v1/concepts/{conceptId}/stores/{storeId}/salaried-jobs Jobs and wages
setSalariedJobs PUT /api/v1/concepts/{conceptId}/stores/{storeId}/salaried-jobs Jobs and wages
setAverageWage PUT /api/v1/concepts/{conceptId}/average-wage Jobs and wages
getEmpAvailability to getEmpAvailabilityV4 GET /api/v1/concepts/{conceptId}/stores/{storeId}/employees/availability Availability
getLaborPositionNames GET /api/v1/concepts/{conceptId}/stores/{storeId}/labor-positions Labor positions
setEmpLaborPositions PUT /api/v1/concepts/{conceptId}/stores/{storeId}/employee-labor-positions Labor positions
syncEmployeeRequiredFieldsUpdates None EmpService methods with no REST equivalent
getSchedule, getSchedule2, getScheduleV3, getScheduleV4, getScheduleV5, getShiftsV3 GET /api/v1/concepts/{conceptId}/stores/{storeId}/schedules Shifts
getScheduleStatus GET /api/v1/concepts/{conceptId}/stores/{storeId}/schedule-status Schedule status
getAppliedTemplateFcstExport GET /api/v1/concepts/{conceptId}/stores/{storeId}/applied-templates Applied templates
getLocations, getTemplates None ScheduleService methods with no REST equivalent
getProjectedSales, getProjectedSalesV3 GET /api/v1/concepts/{conceptId}/stores/{storeId}/projected-sales Projected sales
getApprovedPTO GET /api/v1/concepts/{conceptId}/stores/{storeId}/time-off-occurrences or GET /api/v1/concepts/{conceptId}/stores/{storeId}/time-off Reading approved time off
setApprovedTimeOff, setApprovedTimeOffV2 POST /api/v1/concepts/{conceptId}/stores/{storeId}/time-off Creating approved time off
sayHello (every service) GET /api/v1/health Health check

HierarchyService

Concepts, groups and stores

GET /api/v1/concepts replaces getConcepts and getConceptsV2. GET /api/v1/concepts/{conceptId}/groups replaces getGroups. GET /api/v1/concepts/{conceptId}/stores replaces getStores and getStoresV2.

SOAP REST
getConcepts() / getConceptsV2() GET /api/v1/concepts
getGroups(concept) GET /api/v1/concepts/{conceptId}/groups
getStores(concept, group) / getStoresV2(concept, group) GET /api/v1/concepts/{conceptId}/stores?groupId={groupId}

Responses:

groupId on the stores call is optional. Leave it out, or pass 0, to list every store in the concept. A group-scoped credential sees only its own groups and stores.

Requires: no store permission.

Revenue centers, sales categories and group jobs

SOAP REST
getGroupRVCs(concept, group) GET /api/v1/concepts/{conceptId}/groups/{groupId}/revenue-centers
getGroupSalesCats(concept, group) GET /api/v1/concepts/{conceptId}/groups/{groupId}/sales-categories
getGroupJobs(concept, group) GET /api/v1/concepts/{conceptId}/groups/{groupId}/jobs
setRVC(concept, group, isGroupLevel, store, rvcId, rvcName) PUT /api/v1/concepts/{conceptId}/groups/{groupId}/revenue-centers
setSalesCat(concept, group, isGroupLevel, store, salesCatId, salesCatName) PUT /api/v1/concepts/{conceptId}/groups/{groupId}/sales-categories

Responses:

The two PUT calls take the former SOAP parameters in a JSON body. isGroupLevel becomes groupLevel, and store becomes storeId (a number):

{ "rvcId": 1, "rvcName": "Bar", "groupLevel": true, "storeId": 2231 }
{ "salesCatId": 1, "salesCatName": "Food", "groupLevel": true, "storeId": 2231 }

When groupLevel is false, storeId picks the store that owns the entry. The call creates the entry if its ID is new and updates it otherwise. The name must not be blank and can be at most 50 characters, and the ID must be 0 or more. A violation comes back in the summary as failCount: 1, not as an HTTP error.

Requires: the matching setter enabled for your company (revenue center setter or sales category setter), otherwise 403. Ask Fourth support to enable it. A group-scoped credential needs access to the group.

EmpService

Store employees

SOAP What it was REST
getStoreEmployees Numeric store key, base fields GET /api/v1/concepts/{conceptId}/stores/{storeId}/employees
getStoreEmployeesV2 Numeric store key, adds custom fields GET /api/v1/concepts/{conceptId}/stores/{storeId}/employees
getStoreEmployeesV3 Alphanumeric store key, base fields GET /api/v1/concepts/{conceptId}/stores/{storeId}/employees
getStoreEmployeesV4 Alphanumeric store key, adds custom fields GET /api/v1/concepts/{conceptId}/stores/{storeId}/employees
getStoreEmployeesV5 Alphanumeric store key, adds more fields GET /api/v1/concepts/{conceptId}/stores/{storeId}/employees
getStoreEmployeesV6 Adds excludeASCUsers and permission set and POS role fields GET /api/v1/concepts/{conceptId}/stores/{storeId}/employees
getEmpInfo, getEmpInfoV2 Simplified employee info GET /api/v1/concepts/{conceptId}/stores/{storeId}/employees/info
setEmps, setEmpsV2, setEmpsV3, setEmpsV4 Employee write, each version adding fields PUT /api/v1/concepts/{conceptId}/stores/{storeId}/employees

All of the getStoreEmployees versions are one call, GET /api/v1/concepts/{conceptId}/stores/{storeId}/employees, with the superset of fields. The four setEmps versions are one call, PUT on the same path. getEmpInfo and getEmpInfoV2 are GET .../employees/info.

Reading employees. Query parameters: activeOnly (default false) leaves out terminated employees, and excludeAscUsers (default false) leaves out above-store users. Both apply whichever SOAP version you came from. Requires permission 3009 Get Store Employees.

Fields: posId, hrId, firstName, lastName, nickname, status, address, address2, city, state, zipCode, country, phone, mobile, email, dateOfBirth, hireDate, termDate, canonicalId, permissionSetName, posRoleId, posRoleName, homeStore, payType, payBaseAmount, otExempt, customFields.

Differences from SOAP:

Employee info. GET .../employees/info takes activeOnly and returns posId, hrId, lastUpdated, accountCreated, permissionSetName and assignedSchedules (reason, id). The two timestamps carry an offset. Requires permission 3020 Get Employee Info.

Writing employees. PUT .../employees takes a JSON array of employees. Requires permission 3007 Set Employees. Field mapping from the setEmps family:

SOAP REST
empNum posId
alphanumericHrId hrId
FName, LName, NName firstName, lastName, nickname
address, address2, city, state, zipCode, phone, mobile, email the same names
dob, hireDate, termDate (datetime) dateOfBirth, hireDate, termDate (yyyy-MM-dd)
status 1 / 0 / -1 status ACTIVE / INACTIVE / TERMINATED
customFields (list of name and value) customFields (object: { "Department": "Front of House" })
homeStore, otExempt the same names
payType (for example HOURLY) payType hourly or salaried
salary amount in cents (setEmpsV3, setEmpsV4) payBaseAmount in dollars, with salaryFrequency
hsId, clientId, storeNum, altId not part of the REST payload

Salaried pay: send payBaseAmount as a decimal in the store's main currency unit and salaryFrequency to say what period it covers. The server stores the annual figure.

salaryFrequency payBaseAmount is the amount per
ANNUAL (default) year
MONTHLY month
BI_WEEKLY two weeks
WEEKLY week
HOURLY not valid for a salaried employee; the row is rejected

SOAP rounded the per-period amount to whole cents and then multiplied. REST multiplies first and rounds the annual figure once. If your amounts have no more decimals than the currency allows, you get the same stored value. A null or missing payBaseAmount never overwrites a stored salary. An invalid salary fails that row with an error, where SOAP could skip it silently.

How a sync behaves:

Company and above-store employees

SOAP REST
getCompanyEmployees(activeOnly, aboveStore) GET /api/v1/employees
getAboveStoreEmployees(company) GET /api/v1/employees/above-store

Both need a master credential and no store permission. The company is taken from your credentials.

Company roster. One row per employee per store, so an employee who works at three stores appears three times. Fields: hrId, posId, storeId (the numeric store number), alphanumericStoreId, firstName, lastName, status, homeStore, aboveStore. By default only regular employees are returned. Set aboveStore=true to add above-store employees where they have a status row. Inactive stores are always left out. activeOnly=true keeps rows whose status at that store is active. Differences from SOAP: no internal hsId, status is ACTIVE, INACTIVE or TERMINATED, a missing POS mapping is null instead of -1, and a missing HR ID is null instead of an empty string. This call scans the whole company, so it has a tight rate limit.

Above-store employees. Fields: firstName, lastName, nickname, address, address2, city, state, zipCode, country, phone, mobile, email, status, dateOfBirth, hireDate, hrId, canonicalId, payType, hierarchyNodes (the path from the top of your hierarchy to the employee's node, top first) and securityGroup. status is read at the employee's home store and is UNKNOWN when there is none.

Jobs and wages

SOAP REST Permission
getStoreJobs, getStoreJobsV2 GET /api/v1/concepts/{conceptId}/stores/{storeId}/jobs 3010 Get Store Jobs
getEmpJobs, getEmpJobsV2 to getEmpJobsV6 GET /api/v1/concepts/{conceptId}/stores/{storeId}/employee-jobs 3014 Get Emp Jobs
setEmpJobs, setEmpJobsV2, setEmpJobsV3, setEmpJobsV4 PUT /api/v1/concepts/{conceptId}/stores/{storeId}/employee-jobs 3008 Set Employee Jobs
getSalariedJobs GET /api/v1/concepts/{conceptId}/stores/{storeId}/salaried-jobs 3014 Get Emp Jobs
setSalariedJobs PUT /api/v1/concepts/{conceptId}/stores/{storeId}/salaried-jobs 3008 Set Employee Jobs
setAverageWage PUT /api/v1/concepts/{conceptId}/average-wage none

Store jobs. Returns the store's active jobs: posId, jobName, defaultRate.

Employee jobs, read. Returns storeNum, posEmpId, posJobId, regularWage, overtimeWage, primary, skillLevel, disabled, customFields. skillLevel is the skill level's rank, or null. The wage shown is the dated wage row in force today when the assignment has one, and otherwise the assignment's own wage. A wage written with PUT may therefore not show on read for an assignment that has a dated wage row in force.

Employee jobs, write. Field mapping:

SOAP REST
posEmpId, posJobId, primary the same names
regWage regularWage
ovtWage overtimeWage
skillLevel skillLevel (the skill level rank)
effectiveDateRange (start with day, month, year; regWage; primary), setEmpJobsV3 effectiveDateRanges: a list of start (yyyy-MM-dd), regularWage, primary
hsEmpId, hsJobId, clientId, storeNum, date not part of the REST payload
[
  { "posEmpId": 3001, "posJobId": 4001, "regularWage": 15.50, "overtimeWage": 23.25, "primary": true, "skillLevel": 1 }
]

Behavior to know:

Salaried jobs. Both calls use posEmpId, posJobId and annualSalary. annualSalary is a decimal in dollars (SOAP used cents). A missing, zero, negative or oversized value fails the row. getSalariedJobs returns salaried employees that have a primary job.

Average wage. SOAP setAverageWage(concept, clientData) took a list of locNum and averageWage. REST takes the concept in the path and a JSON array:

[ { "storeId": "100", "averageWage": 18.75 }, { "storeId": "200", "averageWage": 20.00 } ]

storeId is the alphanumeric store ID. averageWage is a decimal of 0 or more. An empty array returns 400. A store that is unknown, inactive or given a negative wage is reported in the summary and does not stop the others. The call needs alphanumeric store ID access enabled for your company, otherwise 403. Ask Fourth support to enable it.

Availability

SOAP REST
getEmpAvailability, getEmpAvailabilityV2, getEmpAvailabilityV3, getEmpAvailabilityV4 GET /api/v1/concepts/{conceptId}/stores/{storeId}/employees/availability

Requires permission 3021 Get Employee Availability. Query parameters: activeOnly (default false) and availabilityType (all, available or unavailable, default all, not case-sensitive, anything else returns 400). availabilityType chooses which range lists each entry carries and never filters employees.

REST returns only the shape of getEmpAvailabilityV4: for each employee, a list of effective-dated calendars. The older per-day shape is not available.

[
  {
    "posId": 100,
    "hrId": "HR-001",
    "availabilities": [
      {
        "effectiveDate": "2025-07-01",
        "unavailabilityRanges": [ { "weekDay": "MON", "startTime": "09:00", "endTime": "17:00" } ],
        "availabilityRanges": [ { "weekDay": "TUE", "startTime": "00:00", "endTime": "00:00" } ],
        "threshold": { "daysInWeekMin": 3, "daysInWeekMax": 5, "hoursInWeekMin": 20.00, "hoursInWeekMax": 40.00, "hoursInDayMax": 10.00, "hoursBetweenShiftMin": 8.00, "consecutiveDaysMax": 6 }
      }
    ]
  }
]

Differences from SOAP: weekDay is MON to SUN, not 1 to 7. Times are HH:mm. An end time of 00:00 means midnight at the end of the day, so 00:00 to 00:00 is the whole day. Threshold hour limits are decimals with two places. Any threshold field can be null. Companies whose business day ends on the calendar day get 501.

Labor positions

SOAP REST
getLaborPositionNames GET /api/v1/concepts/{conceptId}/stores/{storeId}/labor-positions
setEmpLaborPositions PUT /api/v1/concepts/{conceptId}/stores/{storeId}/employee-labor-positions

Both calls need labor positions and position skill levels enabled for your company and store, otherwise 403. The write also needs permission 3051 Set Employee Labor Positions.

The read returns externalId and positionName for the store's active positions that can be assigned to employees. The write keeps the SOAP field names:

[
  { "empHrId": "HR-001", "laborPositions": [ { "laborPositionExtId": "LP-001", "laborPositionName": "Line Cook", "primary": true } ] }
]

The position is found by laborPositionExtId, and laborPositionName must match its name exactly or that assignment is rejected. The employee is found by empHrId. The sync compares your list with what is stored, removes positions that are no longer listed, adds new ones with the lowest enabled skill level, and leaves existing ones alone, so sending the same payload twice writes nothing.

EmpService methods with no REST equivalent

syncEmployeeRequiredFieldsUpdates has no REST endpoint. sayHello is replaced by the health check.

ScheduleService

The REST design does not try to mirror SOAP field for field. Schedules, locations and jobs are identified by name, internal row IDs are not returned, and durations are ISO-8601 strings.

All three schedule calls need permission 3004 Schedule Service, and applied templates also needs 3022 Get Templates. A group-scoped credential needs access to the store.

Shifts

SOAP What it was REST
getSchedule Date bounds GET /api/v1/concepts/{conceptId}/stores/{storeId}/schedules
getSchedule2 Date bounds, adds pay and hours GET /api/v1/concepts/{conceptId}/stores/{storeId}/schedules
getScheduleV3 HSSimpleDate bounds GET /api/v1/concepts/{conceptId}/stores/{storeId}/schedules
getScheduleV4 Adds meal and break minutes GET /api/v1/concepts/{conceptId}/stores/{storeId}/schedules
getScheduleV5 Date and time filter, alphanumeric store ID, adds shift ID GET /api/v1/concepts/{conceptId}/stores/{storeId}/schedules
getShiftsV3 Adds isHouse, isScheduled, isPosted and jobCodes filters GET /api/v1/concepts/{conceptId}/stores/{storeId}/schedules

All six become GET /api/v1/concepts/{conceptId}/stores/{storeId}/schedules.

Parameter Required Notes
startsAfter yes ISO-8601 instant with offset. Shifts that start at or after it.
startsBefore yes ISO-8601 instant with offset. Shifts that start before it (exclusive).
house no true for house shifts only, false for shifts with an owner only, omitted for both

The window must not be longer than 745 hours (31 days and 1 hour), and startsBefore must not be before startsAfter. Otherwise you get 400. This lets you read a whole calendar month in one call.

[
  {
    "scheduleName": "Front of House",
    "locationName": "Patio",
    "startTime": "2026-09-19T08:00:00-05:00",
    "endTime": "2026-09-19T16:00:00-05:00",
    "regularDuration": "PT8H",
    "overtimeDuration": "PT0S",
    "mealDuration": "PT30M",
    "breakDuration": "PT15M",
    "payRate": 15.50,
    "overtimeRate": 23.25,
    "specialPay": 0.00,
    "job": { "externalRef": 200, "name": "Server", "shortName": "SRV" },
    "employee": { "firstName": "Jane", "lastName": "Doe", "posId": 1001, "hrId": "QWE-500", "canonicalId": "FOURTH-EMP-001" }
  }
]

What changes from SOAP:

Schedule status

SOAP REST
getScheduleStatus(concept, ...) GET /api/v1/concepts/{conceptId}/stores/{storeId}/schedule-status

Query parameters: statusDate (a date, defaults to today in the store's time zone) and omitMissing (default false).

[
  {
    "scheduleName": "Server",
    "scheduleStatus": "POSTED",
    "postedAt": "2026-09-18T22:14:00-05:00",
    "postedBy": { "firstName": "Jane", "lastName": "Doe", "posId": 1001, "hrId": "QWE-500", "canonicalId": "FOURTH-EMP-001" },
    "edited": true
  }
]

SOAP had no date parameter, always used the current date, and returned only schedules that had a status. To get the same result, call without statusDate and pass omitMissing=true. Without omitMissing, a schedule with no status for the date comes back as MISSING. The other values are DRAFT and POSTED. postedAt is null if the schedule was never posted, and edited is true when it was posted more than once on that date. The call now requires permission 3004, which SOAP did not check.

Applied templates

SOAP REST
getAppliedTemplateFcstExport(concept, store, weeksForward) GET /api/v1/concepts/{conceptId}/stores/{storeId}/applied-templates

weeksForward is required and must be 1 to 5. It counts whole work weeks after the current one, so 1 is next week only. The current week is never included. Requires permissions 3004 Schedule Service and 3022 Get Templates. SOAP checked only group access, so permission 3004 is new for this call.

SOAP returned a base64-encoded CSV. REST returns JSON with one record per template application, each holding its shifts:

[
  {
    "templateName": "Weekday AM",
    "weekStart": "2026-09-21",
    "appliedAt": "2026-09-14T09:03:00-05:00",
    "appliedBy": { "firstName": "Jane", "lastName": "Doe", "posId": 1001, "hrId": "QWE-500", "canonicalId": "FOURTH-EMP-001" },
    "shifts": [
      {
        "dayPartName": "Lunch",
        "job": { "externalRef": 200, "name": "Server", "shortName": "SRV" },
        "locationName": "Patio",
        "startTime": "2026-09-21T08:00:00-05:00",
        "endTime": "2026-09-21T16:00:00-05:00",
        "shiftDuration": "PT8H",
        "breakDuration": "PT30M",
        "netShiftDuration": "PT7H30M",
        "laborAmount": 124.00
      }
    ]
  }
]

A template applied to three weeks gives three records, so templateName repeats and templateName with weekStart identifies a record. laborAmount is the job's average hourly rate times the full shift length, without subtracting breaks.

ScheduleService methods with no REST equivalent

ProjectedSalesService

Projected sales

SOAP What it was REST
getProjectedSales(concept, store, start, end) Date bounds GET /api/v1/concepts/{conceptId}/stores/{storeId}/projected-sales
getProjectedSalesV3(concept, store, start, end) HSSimpleDate bounds GET /api/v1/concepts/{conceptId}/stores/{storeId}/projected-sales

Both become GET /api/v1/concepts/{conceptId}/stores/{storeId}/projected-sales?start=2026-09-15&end=2026-09-16. start and end are plain dates and both are inclusive. Requires permission 3005 Projected Sales. This service is read-only in SOAP and in REST.

[
  {
    "businessDate": "2026-09-15",
    "total": 12480.50,
    "source": "DAY_PART",
    "periods": [
      {
        "name": "Breakfast",
        "startTime": "06:00:00",
        "endTime": "11:00:00",
        "total": 2100.00,
        "summaryItems": [ { "name": "Food", "total": 1800.00 }, { "name": "Beverage", "total": 300.00 } ]
      }
    ]
  }
]

TimeOffService

Time off in REST comes in two storage forms, shown in the source field. DAY_PART is whole dates by day part. RANGE has a start and end time. Every call needs permission 3050 Time Off Service.

Reading approved time off

SOAP REST
getApprovedPTO(concept, store, start, end) GET /api/v1/concepts/{conceptId}/stores/{storeId}/time-off-occurrences
the same, one item per request instead of per day GET /api/v1/concepts/{conceptId}/stores/{storeId}/time-off

time-off-occurrences is the closest to the SOAP result: a flat list with one item per stretch of time off. time-off returns one record per approved request, with a days list.

start and end are plain dates, both inclusive. paid_only defaults to false.

Differences from SOAP:

Creating approved time off

SOAP REST
setApprovedTimeOff(concept, storeNum, timeOffData) POST /api/v1/concepts/{conceptId}/stores/{storeId}/time-off
setApprovedTimeOffV2(concept, storeId, timeOffData) POST /api/v1/concepts/{conceptId}/stores/{storeId}/time-off

Both versions are one call. Field mapping:

SOAP REST
employeePosId posEmpId
jobId (0 for none) posJobId (leave it out for none)
timeOffTypeCode typeCode
paid, timeOffMinutes, comment the same names
start, end (bare datetime) start, end (ISO-8601 with offset)
[
  { "posEmpId": 1001, "posJobId": 22, "paid": true, "typeCode": "SK", "timeOffMinutes": 480,
    "start": "2026-10-01T00:00:00-05:00", "end": "2026-10-03T00:00:00-05:00", "comment": "HRIS vacation" }
]

A bad item is reported by its index and does not stop the others. The call also needs the store to use time ranges for time off and to allow time-off requests. Creating time off houses (unassigns) the employee's overlapping scheduled shifts. An item that overlaps existing time off for the employee is rejected.

Updating and deleting approved time off

These calls are new. SOAP could only add time off, so correcting a date meant editing it in HotSchedules by hand.

REST
PATCH /api/v1/concepts/{conceptId}/stores/{storeId}/time-off/{timeOffId}
DELETE /api/v1/concepts/{conceptId}/stores/{storeId}/time-off/{timeOffId}

Services without a REST equivalent yet

These SOAP services have no REST endpoint yet. Keep using SOAP for them until their REST equivalents are announced.

ProjectedLaborService is deprecated and has nothing to migrate.

New in REST

Health check

GET /api/v1/health needs no credentials. It replaces the sayHello connectivity test that every SOAP service had.

POS check ingestion

POST /api/v1/concepts/{conceptId}/stores/{storeId}/sales accepts a list of POS check transactions for a store and returns 202 Accepted. It is not a replacement for the SOAP SalesService. Like every other call, it is addressed by concept and alphanumeric store ID, and its moments are ISO-8601 with an offset.

Cutover checklist

  1. Pick Basic or OAuth and confirm a call to GET /api/v1/concepts works with your credentials.
  2. List your stores with GET /api/v1/concepts/{conceptId}/stores and store each alphanumericStoreNum as the storeId you will use.
  3. Use the SOAP method index to map every SOAP method you call to its REST endpoint.
  4. Update your request and response handling for the changes that apply to every call: dates with offsets, decimal amounts, null for "none", per-row results.
  5. Check that the store permissions and company settings named in each section are enabled, and test with a group-scoped credential if you use one.
  6. Add handling for 429 using Retry-After.
  7. Run both integrations side by side on a test store and compare results before switching production.