Skip to content

Developers

Proposed changes

API docs: Proposed changes

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

NameTypeRequiredDefaultAllowed valuesDescription
companyCodestringWhen the key covers more than one companyMax 10 charactersThe company. A key bound to one company can name only its own.
dryRunbooleanNofalsetrue runs every check and saves nothing
changesarrayYes1 to 1,000 itemsThe proposed records. Each one names its entity.

A job record

NameTypeRequiredDefaultAllowed valuesDescription
changes[].entitystringYesjob
changes[].jobCodestringYes1 to 50 charactersThe Job's code. Stored trimmed and in upper case.
changes[].namestringYes1 to 100 charactersThe name
changes[].divisionCodestring or nullNoMax 20 charactersMust be an active division of the company
changes[].addressstring or nullNoMax 300 charactersThe site address
changes[].isActivebooleanNotruefalse proposes the Job as inactive

A phase_code record

NameTypeRequiredDefaultAllowed valuesDescription
changes[].entitystringYesphase_code
changes[].jobCodestringYes1 to 50 charactersThe Job: in Matrix TimeCard, earlier in this request, or already proposed
changes[].phaseCodestringYes1 to 50 charactersThe Phase Code. Trimmed only.
changes[].descriptionstring or nullNoMax 200 charactersThe description
changes[].isActivebooleanNotruefalse proposes the Phase Code as inactive

An equipment record

NameTypeRequiredDefaultAllowed valuesDescription
changes[].entitystringYesequipment
changes[].equipmentCodestringYes1 to 30 charactersThe equipment number. Trimmed, case kept, matched without case.
changes[].descriptionstring or nullNoMax 200 charactersThe description
changes[].divisionCodestring or nullNoMax 20 charactersMust be an active division of the company. It cannot differ from the current division of existing equipment.
changes[].statusstringNoA for new equipmentA, D, IActive, Down or Inactive
changes[].ownershipstring or nullNoowned, rentedOwned or rented
changes[].isActivebooleanNotruefalse 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

outcomeMeaning
stagedWaiting for an administrator. It replaces any older proposal for the same record.
publishedLive now, because the company uses auto-publish. If the publish fails, the proposal stays staged and message says why.
matches_currentMatrix TimeCard already holds exactly this. Nothing is staged. See Records that another source system owns.
unchangedThe same proposal is already waiting. changeId names it.
refusedNot valid. message says why.

On a dry run, a record that would be staged reads staged, even when autoPublished is true.

Response fields

FieldTypeDescription
record.dryRunbooleanTrue when nothing was saved
record.autoPublishedbooleanTrue when the company publishes incoming changes automatically
record.results[].indexintegerThe record's position in changes, starting at 1
record.results[].entitystring or nulljob, phase_code or equipment. Null when the record named no known entity.
record.results[].keystring or nullThe Job code, jobCode/phaseCode, or the equipment code. Null when the record is not valid.
record.results[].outcomestringSee Outcomes
record.results[].changeIdintegerThe proposal that was staged, published or found waiting. Absent on a dry run.
record.results[].messagestringWhy the record was refused, or why it was not published
record.counts.stagedintegerRecords staged
record.counts.unchangedintegerRecords already waiting
record.counts.matchesCurrentintegerRecords already current
record.counts.publishedintegerRecords published
record.counts.refusedintegerRecords refused

Why a record is refused

  • It is not valid.
  • A divisionCode is 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

StatuscodeWhen
400validation_failedchanges 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.
401No key, or a key that is not valid, revoked or expired
403insufficient_scopeThe key does not have reference-changes:write
403forbiddenA key bound to one company names another, or the key has no source system
404not_foundcompanyCode names no active company that the key can reach
500internal_errorThe proposed changes could not be submitted
503service_unavailableThe service cannot check the key at the moment