Send Jobs, Phase Codes and equipment for an administrator to approve before they go live. A proposal does not change live data. It waits on the admin site until an administrator publishes or declines it. If the company publishes incoming changes automatically (auto-publish), the proposal goes live in the same request.
Use this endpoint instead of the direct write endpoints when your company wants a person to check every change first.
Do not mix proposals and direct writes for the same codes. A published proposal is
owned by timecard. A proposal that would change a record that another source system owns
is refused with a message that starts owned_by:<owner>. This includes records that your
own key wrote directly. A proposal that changes nothing on such a record reads
matches_current.
Back to the index · Conventions · Errors
POST /reference-changes
Sends up to 1,000 proposed records for one company. Each record is checked on its own, so one refused record never stops the others. The response has one result per record, in the order sent.
Scope: reference-changes:write. The key must also have a source system. It is
recorded as the proposal's origin.
Order of work. Jobs are handled first, then Phase Codes, then equipment. A
phase_code can name a Job that is sent earlier in the same request.
Field rules. A field that you leave out keeps the record's current value. A blank
string clears a text field. isActive defaults to true.
Request body
| Name | Type | Required | Default | Allowed values | Description |
|---|---|---|---|---|---|
companyCode | string | When the key covers more than one company | Max 10 characters | The company. A key bound to one company can name only its own. | |
dryRun | boolean | No | false | true runs every check and saves nothing | |
changes | array | Yes | 1 to 1,000 items | The proposed records. Each one names its entity. |
A job record
| Name | Type | Required | Default | Allowed values | Description |
|---|---|---|---|---|---|
changes[].entity | string | Yes | job | ||
changes[].jobCode | string | Yes | 1 to 50 characters | The Job's code. Stored trimmed and in upper case. | |
changes[].name | string | Yes | 1 to 100 characters | The name | |
changes[].divisionCode | string or null | No | Max 20 characters | Must be an active division of the company | |
changes[].address | string or null | No | Max 300 characters | The site address | |
changes[].isActive | boolean | No | true | false proposes the Job as inactive |
A phase_code record
| Name | Type | Required | Default | Allowed values | Description |
|---|---|---|---|---|---|
changes[].entity | string | Yes | phase_code | ||
changes[].jobCode | string | Yes | 1 to 50 characters | The Job: in Matrix TimeCard, earlier in this request, or already proposed | |
changes[].phaseCode | string | Yes | 1 to 50 characters | The Phase Code. Trimmed only. | |
changes[].description | string or null | No | Max 200 characters | The description | |
changes[].isActive | boolean | No | true | false proposes the Phase Code as inactive |
An equipment record
| Name | Type | Required | Default | Allowed values | Description |
|---|---|---|---|---|---|
changes[].entity | string | Yes | equipment | ||
changes[].equipmentCode | string | Yes | 1 to 30 characters | The equipment number. Trimmed, case kept, matched without case. | |
changes[].description | string or null | No | Max 200 characters | The description | |
changes[].divisionCode | string or null | No | Max 20 characters | Must be an active division of the company. It cannot differ from the current division of existing equipment. | |
changes[].status | string | No | A for new equipment | A, D, I | Active, Down or Inactive |
changes[].ownership | string or null | No | owned, rented | Owned or rented | |
changes[].isActive | boolean | No | true | false proposes the equipment as inactive |
Example request
curl -X POST "$TIMECARD_BASE_URL/reference-changes" \
-H "X-API-Key: $TIMECARD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"companyCode": "ACME",
"dryRun": false,
"changes": [
{ "entity": "job", "jobCode": "J-2026-120", "name": "Lakeline Parking Lot", "divisionCode": "PAVING" },
{ "entity": "phase_code", "jobCode": "J-2026-120", "phaseCode": "01-100", "description": "Mobilization" },
{ "entity": "equipment", "equipmentCode": "RL-14", "divisionCode": "YARD" }
]
}'
Example response 200 OK
{
"status": 200,
"message": "Success: ACME: 2 staged, 0 published, 0 unchanged, 0 already current, 1 refused.",
"record": {
"dryRun": false,
"autoPublished": false,
"results": [
{
"index": 1,
"entity": "job",
"key": "J-2026-120",
"outcome": "staged",
"changeId": 7311
},
{
"index": 2,
"entity": "phase_code",
"key": "J-2026-120/01-100",
"outcome": "staged",
"changeId": 7312
},
{
"index": 3,
"entity": "equipment",
"key": "RL-14",
"outcome": "refused",
"message": "divisionCode YARD is not an active division of ACME."
}
],
"counts": {
"staged": 2,
"unchanged": 0,
"matchesCurrent": 0,
"published": 0,
"refused": 1
}
}
}
On a dry run, the message starts Success: dry run, nothing was saved:. Do not parse the
message text. It can change.
Outcomes
outcome | Meaning |
|---|---|
staged | Waiting for an administrator. It replaces any older proposal for the same record. |
published | Live now, because the company uses auto-publish. If the publish fails, the proposal stays staged and message says why. |
matches_current | Matrix TimeCard already holds exactly this. Nothing is staged. See Records that another source system owns. |
unchanged | The same proposal is already waiting. changeId names it. |
refused | Not valid. message says why. |
On a dry run, a record that would be staged reads staged, even when autoPublished is
true.
Response fields
| Field | Type | Description |
|---|---|---|
record.dryRun | boolean | True when nothing was saved |
record.autoPublished | boolean | True when the company publishes incoming changes automatically |
record.results[].index | integer | The record's position in changes, starting at 1 |
record.results[].entity | string or null | job, phase_code or equipment. Null when the record named no known entity. |
record.results[].key | string or null | The Job code, jobCode/phaseCode, or the equipment code. Null when the record is not valid. |
record.results[].outcome | string | See Outcomes |
record.results[].changeId | integer | The proposal that was staged, published or found waiting. Absent on a dry run. |
record.results[].message | string | Why the record was refused, or why it was not published |
record.counts.staged | integer | Records staged |
record.counts.unchanged | integer | Records already waiting |
record.counts.matchesCurrent | integer | Records already current |
record.counts.published | integer | Records published |
record.counts.refused | integer | Records refused |
Why a record is refused
- It is not valid.
- A
divisionCodeis not an active division of the company. The message names the code. - The current record belongs to another source system, and the proposal would change it.
The message starts
owned_by:<owner>. - A Job code is already held by a Work Order or a Meet Point.
- Existing equipment has a different
divisionCode. - A Phase Code's Job is not in Matrix TimeCard, not in this request and not proposed.
Records that another source system owns
A proposal for such a record is compared with what Matrix TimeCard shows, with an
administrator's changes applied. Text is compared without surrounding spaces, and a blank
value is equal to null. If nothing would change, the outcome is matches_current and
nothing is staged. So you can send the current values back without a refusal. When an
equipment code is in more than one division, the proposal is compared with the record of
its own divisionCode.
Errors
| Status | code | When |
|---|---|---|
| 400 | validation_failed | changes is missing, empty or has more than 1,000 items, or companyCode is missing when the key covers several companies. A record that is not valid is refused in results, not here. |
| 401 | No key, or a key that is not valid, revoked or expired | |
| 403 | insufficient_scope | The key does not have reference-changes:write |
| 403 | forbidden | A key bound to one company names another, or the key has no source system |
| 404 | not_found | companyCode names no active company that the key can reach |
| 500 | internal_error | The proposed changes could not be submitted |
| 503 | service_unavailable | The service cannot check the key at the moment |