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:
- Getting started explains how HotSchedules data is organized, how to sign in and make a first call, and the rules every call follows.
- API reference describes every call, grouped by what it does, with a link to its entry in Swagger UI. Swagger has the full request and response schemas and lets you try calls from the browser.
- Migrating from the legacy SOAP API is for teams that already use the SOAP services. Every call in the reference names the SOAP method it replaces and links to the field-by-field mapping.
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.
- A concept is a brand or business line of your company.
- A group is a set of stores inside a concept, for example a region or a district.
- A store is one location where employees work. Employees, jobs, schedules, forecasts and time off all belong to a store.
Around the stores, HotSchedules keeps the data a workforce system needs:
- Employees have a profile (name, contact details, hire and termination dates, status) and two identifiers you will see everywhere:
posId, the number your POS uses for the employee, andhrId, the ID from your HR system. - Jobs are the roles people work, such as Server or Line Cook. Each job has a default rate. An employee can hold several jobs at a store, each with its own wage, and one of them is the primary job.
- Schedules hold shifts: who works which job, where and when. A schedule can be a draft or posted to staff.
- Time off, availability and labor positions describe when employees can work and what they are qualified to do.
- Forecasts (projected sales) and POS checks (actual sales) feed labor planning.
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
-
JSON everywhere. Requests and responses are JSON. Lists are returned as JSON arrays.
-
Dates and times. A calendar date is
yyyy-MM-dd. A moment in time is ISO-8601 with an explicit offset, for example2026-01-01T00:00:00-06:00. Any correct offset works, and responses use the store's time zone. In a query string, write a+offset as%2B, or the server reads it as a space. A range of plain dates (startandend) includes the end date. -
Money. Amounts are decimal numbers in the store's main currency unit, such as dollars:
52000.00. This holds for reading and writing. -
Missing values are
null. A field with no value comes back asnull, not-1or an empty string. A few documented exceptions keep a number: a concept with no external reference has anextIdof-1, and an employee with no POS mapping has aposIdof0, and a job with no external reference has anexternalRefof0, in schedule and time-off responses. -
Writing a list. Calls that write a list of rows, such as employees or jobs, are syncs. HotSchedules compares your list with what it stores and writes only the difference, so sending the same payload twice changes nothing and retrying is safe.
-
One result per row. A sync returns
200with a summary, and a bad row does not stop the others.indexis the position of the row in your request:{ "successCount": 9, "failCount": 1, "errors": [ { "index": 3, "message": "Missing required field: firstName" } ] }
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
- Keep employees in step with your HR system. Read the stores you need, then call PUT /api/v1/concepts/{conceptId}/stores/{storeId}/employees with the employees from your HR system, and PUT /api/v1/concepts/{conceptId}/stores/{storeId}/employee-jobs to set each employee's jobs and wages. Run both on a schedule. They only write what changed.
- Feed payroll or time keeping with the schedule. Read shifts with GET /api/v1/concepts/{conceptId}/stores/{storeId}/schedules one calendar month at a time, and check which schedules managers have posted with GET /api/v1/concepts/{conceptId}/stores/{storeId}/schedule-status.
- Send approved leave from your HRIS. Post it with POST /api/v1/concepts/{conceptId}/stores/{storeId}/time-off. Correct or cancel it later with the update and delete calls, using the
timeOffIdfrom a read. - Build a workforce data warehouse. Read stores, employees, jobs, schedules and projected sales per store on a timer, and stay under the rate limits.
- Send actual sales from your POS in realtime. Post each closed check for a store as it closes with POST /api/v1/concepts/{conceptId}/stores/{storeId}/sales.
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:
- A master credential reaches every store in the company.
- A group-scoped credential reaches only the stores in its groups. A call for any other store returns
403with the messageYou don't have access to this location. GET /api/v1/employeesandGET /api/v1/employees/above-storeneed a master credential. A group-scoped credential gets403.- OAuth tokens are company-wide. If you rely on group-scoped credentials today, keep using Basic with those credentials.
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}/...
conceptIdis the concept's external reference, theextIdreturned by GET /api/v1/concepts.groupIdis the group's external reference, theextIdreturned by GET /api/v1/concepts/{conceptId}/groups.storeIdis the store's alphanumeric store ID, returned asalphanumericStoreNumby GET /api/v1/concepts/{conceptId}/stores. The numericstoreNumthat older SOAP methods accepted matches only when the store's alphanumeric ID has the same value. Read the IDs fromgetStoresrather than assuming.
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
- SOAP used
java.util.Datevalues,HSSimpleDateelements (day,month,year) and bare local datetimes with no time zone. REST never accepts or returns a bare local datetime. - Calendar dates are
yyyy-MM-dd. - Instants are ISO-8601 with an explicit offset, for example
2026-01-01T00:00:00-06:00. Any correct offset works. The server converts it to the store's time zone. - Responses carry an offset too, in the store's time zone.
- In a query string, encode a
+offset as%2B(for example2026-01-01T00:00:00%2B01:00), or the server reads it as a space. - Date ranges that take plain dates (
start,end) include the end date.
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.
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:
- Concept:
id,extId,name.extIdis-1when the concept has no external reference. UseextIdasconceptIdin later calls. - Group:
extId,name,conceptExtId. UseextIdasgroupId. - Store:
storeNum,storeName,active,groupExtId,groupName,streetAddress1,streetAddress2,city,stateProvince,postalCode,timeZone,alphanumericStoreNum. UsealphanumericStoreNumasstoreIdin later calls.
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:
- Revenue center and sales category:
extId,name,groupLevel. - Group job:
hsId,posId,jobName,defaultRate. Only active jobs are returned.
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:
- SOAP
empNumisposId. The internal row IDhsIdis no longer returned. UsehrIdandposIdto identify an employee. A missing HR ID isnull, not-1. statusisACTIVE,INACTIVEorTERMINATED, not1,0or-1.posRoleIdandposRoleNamecome from the employee's own POS role assignment, not from the permission set.posRoleIdis the role's external reference and isnullif the role has none.payTypeishourlyorsalaried, taken from the employee's labor law attribute, not from whether a salary record exists.payBaseAmountis a decimal in dollars, ornullwhen there is none (SOAP used the string"-1").- System and support users are never listed.
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:
- It creates an employee when the POS ID (or the HR ID, for companies that identify employees by HR ID) is not found, and updates it otherwise.
- Only the fields enabled in your store's integration field list are written. Other fields in the request are ignored. Status is always applied.
- The country always comes from the store. A
countryin the request is ignored. - A sync does not move an employee to another store.
- Each row succeeds or fails on its own, and the response lists failures by index.
- Employee-change events are published for every employee the call created or changed. An unchanged replay publishes nothing.
- Routing of employee updates through the FP queue, which some SOAP companies used, is not carried over. The REST call writes straight to HotSchedules. Ask Fourth support if you depend on that queue.
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:
- Employees are matched by POS ID and jobs by job external reference.
- A missing
regularWageorovertimeWagekeeps the stored value. A new assignment gets a wage of 0 and a 1.5x overtime multiplier. A missingskillLevelkeeps the stored one, or uses the default skill level for a new assignment. An unknown rank fails the row. primary: truemakes the job primary.falseor missing leaves the primary job alone.- For every employee in your payload, jobs that are not in the payload are removed, but only jobs that have an external reference. Jobs created inside HotSchedules are kept. Removing an assignment does not yet unassign the employee's shifts for that job or cancel open shift trades.
effectiveDateRangesapplies only the range in force today, meaning the lateststarton or before today. A range that starts in the future fails the row. SOAPsetEmpJobsV3accepted ranges but wrote a zero wage. REST does not keep a wage history.
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:
- Only scheduled, posted shifts are returned, for every job. The
isScheduled,isPostedandjobCodesfilters ofgetShiftsV3are gone. Filter the response yourself if you need a subset of jobs. - A house shift has
employee: null. That is the only marker, there is nohousefield. A house shift still has its own timing, pay and break fields. - A shift's owner, a schedule's
postedByand a template'sappliedByall use the same employee shape:firstName,lastName,posId,hrId,canonicalId. A missing POS mapping givesposId0(SOAP used-1).hrIdandcanonicalIdarenullwhen there is none. - Schedule, location and job are given by name. There is no
scheduleId,roleId,locationId,shiftIdor jobhsId.locationNameis on every shift. - Durations (
regularDuration,overtimeDuration,mealDuration,breakDuration) are ISO-8601 strings likePT7H30M, not minute counts. A zero duration isPT0S. - Break and meal durations come from the break rows stored on the shift. There is no fallback to your break policy.
overtimeRateis the overtime pay value when the shift has overtime and0otherwise.- Results are sorted by start time, then end time, then schedule name.
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
getLocationshas no endpoint. Each shift now carries itslocationName.getTemplates(template definitions) has no endpoint. Use applied templates for the shifts a template projects.sayHellois replaced by the health check.
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 } ]
}
]
}
]
- There is one entry per business date in the range.
sourceisDAY_PARTwhen the store has day-part forecasts, and the periods are its day parts with a breakdown by summary item. OtherwisesourceisCLIENT_SHIFT, the periods are the store's shifts (defined on the store and on its group), andsummaryItemsis empty. - When the store has any projected sales in the range, every date in the range is returned, and a date with no data has a
totalof0. If the store has none anywhere in the range, the result is an empty array. - The end date is fully included. The SOAP query used an exclusive upper bound that callers never moved forward, so SOAP dropped the last day.
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:
- REST also returns approved unpaid time off, with 0 paid hours. SOAP returned paid time off only. Pass
paid_only=trueto get the same set as SOAP. - Each record has a stable
timeOffId, which SOAP did not have. You need it to update or delete a record. - Time off with a start and end time is cut at midnight in the store's time zone, and every day in the window gets its own item with its hours. SOAP gave a record's last partial day no item of its own, and lost the hours of ranges saved in another time zone.
- Hours are in
paidHours. Day-part items addstartandendtaken from the day parts' times. - When there is no job, SOAP returned job ID
-1and job nameN/A. REST returnsnullforposJobIdandjobName. When an employee has no POS mapping, SOAP returned-1and REST returns0forposId. - The store number is not repeated on each row, because it is in the path.
- SOAP hid the days of inactive employees. REST does not filter by employee status.
- Time off of employees borrowed from another store is included when the store shares employees, as in 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} |
PATCHtakesstart,end, an optionaltimeOffMinutesand an optionalreason, and returns the updated record. If the new range overlaps other time off for the employee, it returns409unless you passoverride_overlap=true. It needs the store to use time ranges for time off.DELETEreturns204. The record is marked deleted and disappears from reads. A deleted record no longer counts as an overlap.- Both work only on approved time-off records with a start and end time. A
DAY_PARTrecord or a record that is not approved returns409. A record with a day in a closed or locked pay period returns400. An unknowntimeOffIdreturns404.
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.
SalesServiceTimeCardServiceVolumeServiceLaborServiceCertificationServiceOperatingBudgetPlanServicePunchRecordService
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
- Pick Basic or OAuth and confirm a call to GET /api/v1/concepts works with your credentials.
- List your stores with GET /api/v1/concepts/{conceptId}/stores and store each
alphanumericStoreNumas thestoreIdyou will use. - Use the SOAP method index to map every SOAP method you call to its REST endpoint.
- Update your request and response handling for the changes that apply to every call: dates with offsets, decimal amounts,
nullfor "none", per-row results. - 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.
- Add handling for
429usingRetry-After. - Run both integrations side by side on a test store and compare results before switching production.