Retrieve employee wage records — the hourly rate of each employee, per role.
| API name | labor.employee.wage |
| Version | v1 |
| Method | POST |
| Response format | JSON |
1. Getting started#
1.1 Credentials#
To call this API you need two values, issued by OrderPin:
| Credential | Sent as | Description |
|---|---|---|
| Client ID | X-Client-ID header |
Identifies your account |
| API key | Authorization: Bearer <api-key> header |
Secret key — keep it confidential |
Every request must include both. Requests are automatically scoped to the data of the store associated with your credentials; you cannot read another store's data, and you do not need to specify it.
Getting credentials: email api@orderpin.us with your Store ID — that is all OrderPin needs to issue your Client ID and API key. Credentials are issued per store: if your company has several stores, request a separate set for each Store ID.
1.2 Endpoint#
There are two equivalent ways to call the API.
Path form (recommended) — the API name is part of the URL:
POST https://{base-url}/orderpin/oapi/labor/employee/wage/v1
The version segment is optional — POST https://{base-url}/orderpin/oapi/labor/employee/wage uses
the current default version (v1). Pinning the version explicitly is recommended so that your
integration is not affected by future default-version changes.
Generic form — call the base endpoint and declare the API in the request body:
POST https://{base-url}/orderpin/oapi
{
"subject": "labor.employee.wage",
"version": "v1",
"filter": { "employeeId": "E10086" }
}
With this form, subject is required (an unknown or missing subject/version is reported with
code 1020); version is optional and defaults to v1. All other parameters (§3) are the same.
The path form is preferred.
1.3 Request headers#
| Header | Required | Value |
|---|---|---|
X-Client-ID |
Yes | Your client ID |
Authorization |
Yes | Bearer <your-api-key> |
Content-Type |
Yes | application/json |
A JSON request body is always required, even when you have no filters to send — use
{}. A request with an empty body is rejected.
2. Request format#
{
"filter": {
"<parameter>": "<value or array of values>"
},
"skip": 0,
"limit": 100,
"sort": [
{ "field": "<field name>", "type": "desc" }
],
"with_record_count": true
}
Example#
POST /orderpin/oapi/labor/employee/wage/v1 HTTP/1.1
Host: {base-url}
X-Client-ID: US00000001
Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxx
Content-Type: application/json
{
"filter": { "employeeId": ["E10086", "E10087"] },
"sort": [{ "field": "createAt", "type": "desc" }],
"with_record_count": true
}
3. Request parameters#
Unrecognized parameters are rejected. Only the parameters listed below are accepted.
3.1 Query parameters#
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
subject |
string | No | — | Only needed if you call the generic endpoint POST /orderpin/oapi; set it to labor.employee.wage. Not required when using the endpoint in §1.2. |
version |
string | No | v1 |
Only used together with subject on the generic endpoint. |
filter |
object | Yes | — | One or more filter parameters (§3.2). At least one filter is required; a request with an empty filter object is rejected. Multiple filters are combined with logical AND. |
skip |
integer | No | 0 |
Number of records to skip. Use together with limit to page through results. |
limit |
integer | No | 200 |
Maximum number of records to return, capped at 200. Values above 200 or below 1 are treated as 200. |
sort |
object or array of objects | No | none | Sort order. Each item is { "field": "<name>", "type": "asc" \| "desc" } — both keys are required. See §5 for sortable fields. |
with_record_count |
boolean | No | false |
When true, the response includes recordCount: the total number of records matching your filter, ignoring skip and limit. |
3.2 Filter parameters#
| Parameter | Type | Accepted form | Description |
|---|---|---|---|
uid |
string | single value or array | Match wage records by their uid. Empty strings are rejected. |
employeeId |
string | single value or array | Match wage records by employee. Empty strings are rejected. |
roleId |
string | single value or array | Match wage records by role. Empty strings are rejected. |
createAt |
integer (epoch ms) | single value or array | Match wages that took effect at exactly this timestamp. Must be greater than 0 and at least 15 minutes in the past (see Data freshness below). |
createStartAt |
integer (epoch ms) | single value | Start of an effective-date range (inclusive). Must be sent together with createEndAt. |
createEndAt |
integer (epoch ms) | single value | End of an effective-date range (exclusive). Must be sent together with createStartAt. |
modifiedAt |
integer (epoch ms) | single value or array | Match wages last modified at exactly this timestamp. Must be greater than 0 and at least 15 minutes in the past. |
modifiedStartAt |
integer (epoch ms) | single value | Start of a last-modified range (inclusive). Must be sent together with modifiedEndAt. |
modifiedEndAt |
integer (epoch ms) | single value | End of a last-modified range (exclusive). Must be sent together with modifiedStartAt. |
All timestamps are Unix epoch time in milliseconds (UTC).
Note:
createAtreflects the wage's effective start time, not the moment the record was written. An employee may have several wage records over time — sort bycreateAtto find the latest.
Data freshness#
Reporting data becomes queryable approximately 15 minutes after it is recorded. A timestamp
filter value that is less than 15 minutes (900000 ms) old — or in the future — is rejected with
data from the last 15 minutes is not yet available for query. When polling for new data, end your
query range at now - 15 minutes.
Using date ranges#
createStartAt / createEndAt and modifiedStartAt / modifiedEndAt are paired range filters:
- Both halves must be sent together. Sending only one is rejected with
missing pair filter. - The range is half-open:
start <= value < end. startmust be earlier thanend.- The span must be less than 31 days (
2678400000ms). Wider ranges are rejected. - Each half must also satisfy the data freshness rule above.
This makes range filters ideal for incremental synchronization: poll with
modifiedStartAt / modifiedEndAt to pick up records that changed since your last run.
Value types#
Values are type-checked. Sending a number where a string is expected (or vice versa) is rejected —
for example, employeeId must be a string, and modifiedAt must be a number.
When a filter accepts an array, it behaves as "matches any of these values".
4. Response format#
A successful request returns HTTP 200 with a JSON object:
{
"employeeWage": [
{
"uid": "W3001",
"employeeId": "E10086",
"employeeName": "Alice",
"roleId": "R2001",
"roleName": "Cashier",
"currency": "USD",
"hourlyWage": 1800,
"modifiedAt": 1767254400000,
"createAt": 1735689600000
}
],
"recordCount": 12
}
| Key | Type | Description |
|---|---|---|
employeeWage |
array | The matching wage records. May be empty; treat a missing key as an empty list. |
recordCount |
integer | Total matching records. Only present when with_record_count is true. |
4.1 Wage fields (employeeWage[])#
| Field | Type | Description |
|---|---|---|
uid |
string | Unique identifier of the wage record. |
employeeId |
string | Identifier of the employee. Matches uid in the Employee API. |
employeeName |
string | Employee display name. May be empty if no name is recorded. |
roleId |
string | Identifier of the role this wage applies to. |
roleName |
string | Name of the role. May be empty if no role name is recorded. |
currency |
string | Currency code, e.g. USD. |
hourlyWage |
integer | Hourly wage in the currency's minor units (e.g. cents — 1800 = 18.00). |
modifiedAt |
integer (epoch ms) | Time the record was last modified. |
createAt |
integer (epoch ms) | Time the wage took effect. |
5. Sorting and pagination#
5.1 Sorting#
sort accepts a single object or an array of objects (applied in order):
"sort": [
{ "field": "createAt", "type": "desc" },
{ "field": "uid", "type": "asc" }
]
type must be asc or desc, and both field and type are required.
Sortable fields: uid, employeeId, createAt, modifiedAt.
Sorting is supported only on these fields. Using any other name is rejected:
- A valid response field that does not support sorting (e.g.
roleId,hourlyWage) →field <name> not sortable - A name that is not a response field (including filter names such as
modifiedStartAt) →unknown sort field <name>
5.2 Pagination#
Results are limited to 200 records per request. To page through a larger result set:
- Send the first request with
"with_record_count": true,"skip": 0,"limit": 200. - Read
recordCountfrom the response to determine the number of pages. - Repeat, increasing
skipbylimiteach time, until all records are retrieved.
Always use a sort when paginating so that the ordering is stable across pages.
6. Errors#
Successful requests return HTTP 200. Errors return the HTTP status that matches the error
category, with a JSON body:
{
"code": 1004,
"msg": "Error: [labor.employee.wage.v1] filter employeeId: employeeId is empty [\"\"]"
}
Read the code field for the precise reason. The HTTP status groups errors by category: 4xx for
request and credential problems, 429 for rate limiting, 5xx for server-side failures.
| HTTP | Code | Meaning | What to do |
|---|---|---|---|
401 |
2004 |
Missing X-Client-ID header |
Add the header to every request. |
401 |
2003 |
Missing or malformed Authorization header |
Use the exact form Bearer <api-key>. |
401 |
2001 |
Unknown client ID | Verify the client ID issued to you. |
401 |
2005 |
Invalid API key | Verify the key; email api@orderpin.us to have it reissued if lost. |
403 |
2006 |
Open API is not enabled for your account | Email api@orderpin.us with your Store ID to have the API enabled. |
400 |
1004 |
Invalid request parameter | The msg field names the offending parameter and reason — see §6.1. |
200 |
1020 |
Unknown API name or version | Check the endpoint path, subject and version. Note this case still returns HTTP 200, so always check the body code. |
429 |
2012 |
Rate limit exceeded | Honor the Retry-After header — see §7. |
500 |
1003 |
Data retrieval failed | Transient — retry with backoff. |
500 |
1012 |
Internal error | Transient — retry with backoff; email api@orderpin.us if it persists. |
6.1 Common 1004 validation messages#
| Message | Cause |
|---|---|
unknown request param: <name> |
A parameter outside §3.1 was sent. |
no filter |
filter is empty or missing. At least one filter is required. |
unknown filter key: <name> |
A filter outside §3.2 was sent. |
filter uid: uid is empty [""] |
An empty string was sent for uid. |
filter employeeId: employeeId is empty [""] |
An empty string was sent for employeeId. |
filter roleId: roleId is empty [""] |
An empty string was sent for roleId. |
filter <name>: less than 0 [<value>] |
A timestamp filter is <= 0. |
filter <name>: data from the last 15 minutes is not yet available for query [<value>] |
A timestamp filter is less than 15 minutes old — see Data freshness (§3.2). |
missing pair filter [<name>] |
A range filter was sent without its counterpart. |
END value less than START |
The range end is not after the range start. |
between range greater than 2678400000 |
The range spans 31 days or more. |
expect string but got: <value> |
A filter value of the wrong type (e.g. a number sent for employeeId). |
parse sort field error: field <name> not sortable |
sort named a response field that does not support sorting (§5.1). |
parse sort field error: unknown sort field <name> |
sort named something that is not a response field. |
no sort field / sort type expect [asc \| desc] |
A sort item is missing field, or type is not asc/desc. |
7. Rate limits#
Requests are limited per client ID, and the quota is shared across the whole service — running several workers in parallel does not give you extra capacity.
The exact values are configured per environment. Read them from the response headers rather than hard-coding them:
| Header | Returned on | Meaning |
|---|---|---|
X-RateLimit-Limit |
every response | Your quota, i.e. the maximum number of requests per window. |
X-RateLimit-Remaining |
every response | Quota left in the current window. Best-effort — it can lag slightly under concurrent load. |
Retry-After |
429 only |
Whole seconds to wait before retrying. |
When you exceed the quota the response is:
HTTP/1.1 429 Too Many Requests
Retry-After: 37
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
{ "code": 2012, "msg": "[2012] Error: rate limit exceeded, retry after 37 second(s)" }
The window slides continuously rather than resetting at a fixed boundary, so quota comes back
gradually. Waiting the full Retry-After is always sufficient.
How to stay within the limit#
- Retry on
429usingRetry-After, not a fixed sleep. Add jitter if you run multiple workers. - Watch
X-RateLimit-Remainingand slow down before you hit zero. - Prefer wide queries over many narrow ones. A single request with
employeeIdas an array andlimit: 200costs one unit of quota and returns up to 200 records. - Do not retry
5xxin a tight loop — those attempts also consume quota.
Rate limiting is applied only after your credentials have been verified, so a request that fails authentication does not consume your quota.
8. Recommendations#
- Filter by
modifiedStartAt/modifiedEndAtfor incremental synchronization. - Keep ranges under 31 days. Split longer periods into multiple requests.
- Always send a
sortwhen paginating so pages do not overlap or skip records. - Request
with_record_countonce (on the first page) rather than on every page. - Wage history: an employee can have multiple wage records; the one with the latest
createAt(effective start) not in the future is the current rate. - Protect your API key. Send it only over HTTPS, never embed it in client-side code, and email api@orderpin.us immediately if you suspect it has been exposed.
- Retry transient failures (
1003,1012) with exponential backoff, and honorRetry-Afteron429.
9. Full example#
curl -X POST 'https://{base-url}/orderpin/oapi/labor/employee/wage/v1' \
-H 'X-Client-ID: US00000001' \
-H 'Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxx' \
-H 'Content-Type: application/json' \
-d '{
"filter": { "employeeId": "E10086" },
"skip": 0,
"limit": 200,
"sort": [{ "field": "createAt", "type": "desc" }],
"with_record_count": true
}'