Skip to content

Developers

Activities

API docs: Activities

An activity is anything a worker clocks in to: a Job, a Work Order or a Meet Point. All three use the same /activities endpoints. The kind field tells them apart.

KindPhase CodesNotes
JobOptionalCan carry a reporting type, a reporting frequency and a program close date
WorkOrderNoneCan link to a Job (jobCode) and to one piece of equipment (equipmentCode)
MeetPointAt least oneCan be shown to some crews only (audience)

Scopes: activity:read to read, activity:write to write.

Back to the index · Conventions · Errors

Every endpoint can also return 401 (no key, or a key that is not valid, revoked or expired) and 503 (service_unavailable or instance_mismatch). The tables below list only the other statuses.

The Activity object

GET /activities/{activityId} returns this object in record. GET /activities returns a list of them in record.records.

FieldTypeDescription
activityIdintegerThe activity's id
kindstringJob, WorkOrder or MeetPoint
companyCodestringThe company
sourceSystemstringThe owner. See Ownership.
codestringTrimmed and upper case. It never changes.
namestringThe name that crews see. The effective value: an administrator's override where one is set.
addressstring or nullThe site address (effective value)
latitudenumber or nullThe site point, WGS 84 degrees (effective value). latitude and longitude are both set or both null.
longitudenumber or nullSee latitude
divisionCodestring or nullThe division
timeZonestring or nullThe site's IANA time zone. Null means the account's default time zone.
isActivebooleanFalse when the activity is closed
completeStatusstring or nullIncomplete or Complete on a Work Order. The phone hides a Complete Work Order.
jobCodestring or nullA Work Order's Job. Null on other kinds.
isJobKnownbooleanTrue when jobCode names a Job in Matrix TimeCard
equipmentCodestring or nullA Work Order's equipment. Null on other kinds.
isLocallyOverriddenbooleanTrue when an administrator overrides a field
overriddenarray of stringsThe fields that an override covers
sourceValues.namestring or nullThe owner's own name, under any override
sourceValues.addressstring or nullThe owner's own address
sourceValues.latitudenumber or nullThe owner's own latitude
sourceValues.longitudenumber or nullThe owner's own longitude
phaseCodes[].codestringAn active Phase Code. Only active Phase Codes are listed, ordered by code.
phaseCodes[].descriptionstring or nullIts description
reportingTypestring or nullA Job's reporting type. See Reporting types. Always null on other kinds.
reportingFrequencystring or nullA Job's reporting frequency. See Reporting frequencies. Always null on other kinds.
programClosesOnstring or nullYYYY-MM-DD, the day a Job's program closes. Null while it is open.
audience[].divisionIdinteger or nullA Meet Point audience pair: the division's id
audience[].divisionCodestring or nullThe division code. Null means any division.
audience[].payClassIdinteger or nullThe pay class's id
audience[].payClassCodestring or nullThe pay class code. Null means any pay class.
geofences[].geofenceIdintegerA boundary's id. Every boundary is listed, active or not.
geofences[].isActivebooleanThe phone uses only active boundaries
geofences[].isPublishedbooleanThe boundary has a live version, so phones use it
geofences[].hasDraftPendingbooleanA new version waits for an administrator to publish it
geofences[].sourceSystemstringThe boundary's owner
geofences[].isLocallyManagedbooleanAn administrator took the boundary over. Your key cannot change it.
updatedAtstringISO 8601 UTC, to the millisecond. Send it back as expectedUpdatedAt.

audience is empty on a Job or Work Order. On a Meet Point, an empty audience means everyone sees it.


GET /activities

Lists Jobs, Work Orders and Meet Points, ordered by code. Each record includes its active Phase Codes and its boundaries.

Scope: activity:read

Query parameters

NameTypeRequiredDefaultAllowed valuesDescription
kindstringNoAll threeJob, WorkOrder, MeetPoint, or a comma listThe kinds to list. Case does not matter.
isActivestringNoBothtrue, falseKeep only active or only inactive activities
qstringNoMax 100 charactersSearch the code, name and address. Longer text is cut to 100 characters.
companyCodestringNoMax 10 charactersKeep only this company
divisionCodestringNoMax 20 charactersKeep only this division
sourceSystemstringNoEvery ownerMax 20 charactersKeep only records that this owner holds, for example your own
limitintegerNo251 to 100Page size
offsetintegerNo00 or moreRows to skip
allstringNofalsetrue, falsetrue returns every match in one response, up to 10,000. Do not send limit or offset with it.
levelintegerNo1Accepted for compatibility. Any other value gets 400.

Example request

curl "$TIMECARD_BASE_URL/activities?kind=Job&isActive=true&limit=100&offset=0" \
  -H "X-API-Key: $TIMECARD_API_KEY"

Example response 200 OK

{
  "status": 200,
  "message": "Success: activities retrieved.",
  "record": {
    "total": 1,
    "records": [
      {
        "activityId": 48213,
        "kind": "Job",
        "companyCode": "ACME",
        "sourceSystem": "acme-erp",
        "code": "J-2026-104",
        "name": "Riverside Bridge Widening",
        "address": "1200 River Rd, Austin, TX 78701",
        "latitude": 30.2672,
        "longitude": -97.7431,
        "divisionCode": "PAVING",
        "timeZone": "America/Chicago",
        "isActive": true,
        "completeStatus": null,
        "jobCode": null,
        "isJobKnown": false,
        "equipmentCode": null,
        "isLocallyOverridden": false,
        "overridden": [],
        "sourceValues": {
          "name": "Riverside Bridge Widening",
          "address": "1200 River Rd, Austin, TX 78701",
          "latitude": 30.2672,
          "longitude": -97.7431
        },
        "phaseCodes": [
          { "code": "01-100", "description": "Mobilization" },
          { "code": "02-200", "description": "Earthwork" }
        ],
        "reportingType": null,
        "reportingFrequency": null,
        "programClosesOn": null,
        "audience": [],
        "geofences": [
          {
            "geofenceId": 9120,
            "isActive": true,
            "isPublished": false,
            "hasDraftPending": true,
            "sourceSystem": "acme-erp",
            "isLocallyManaged": false
          }
        ],
        "updatedAt": "2026-11-03T15:04:05.123Z"
      }
    ]
  }
}

With all=true, record.total is the number of records returned.

Errors

StatuscodeWhen
400validation_failedkind names something else, level is not 1, isActive, limit or offset is out of range, or all=true matches more than 10,000 records
403insufficient_scopeThe key does not have activity:read
500internal_errorThe activities could not be read

GET /activities/{activityId}

Returns one Job, Work Order or Meet Point.

Scope: activity:read

Path parameters

NameTypeRequiredDefaultAllowed valuesDescription
activityIdintegerYes1 or moreThe activity's id

Example request

curl "$TIMECARD_BASE_URL/activities/48213" \
  -H "X-API-Key: $TIMECARD_API_KEY"

Example response 200 OK

{
  "status": 200,
  "message": "Success: activity retrieved.",
  "record": {
    "activityId": 48213,
    "kind": "Job",
    "companyCode": "ACME",
    "sourceSystem": "acme-erp",
    "code": "J-2026-104",
    "name": "Riverside Bridge Widening",
    "updatedAt": "2026-11-03T15:04:05.123Z"
  }
}

The record has every field of the Activity object. It is shortened here.

Errors

StatuscodeWhen
400validation_failedactivityId is not a positive integer
403insufficient_scopeThe key does not have activity:read
404not_foundNo activity has this id, or it is outside the key's companies
500internal_errorThe activity could not be read

POST /activities

Creates one Job, Work Order or Meet Point, with its Phase Codes and an optional job-site boundary. Either all of it is saved or nothing is. The new record is owned by your key's source system.

Scope: activity:write. A drawn location.boundary also needs geofences:write. A point and radius needs no extra scope. With geofences:publish, the boundary is live at once. Without it, the boundary is a draft until an administrator publishes it.

Request body

NameTypeRequiredDefaultAllowed valuesDescription
kindstringYesJob, WorkOrder, MeetPointThe kind of activity
codestringYesMax 50 charactersYour code, for example your job number. Stored trimmed and in upper case. Must be free in the company.
namestringYesMax 100 charactersThe name that crews see on the phone
companyCodestringNoMax 10 charactersThe company. Required when the key covers more than one company.
divisionCodestringNoMax 20 charactersThe division. Phase Codes copy it.
addressstringNoMax 300 charactersThe site address
timeZonestringNoMax 64 characters, IANA nameFor example America/Chicago
phaseCodesarrayNo. A Meet Point needs at least one.Max 1,000 itemsThe Phase Codes. Repeats are dropped (case does not matter). Not allowed on a Work Order.
phaseCodes[].codestringYesMax 50 charactersTrimmed only. Case and inner spaces are kept.
phaseCodes[].descriptionstring or nullNoMax 200 charactersThe description
locationobjectNoThe job-site boundary. Send a point and radius, or a drawn boundary.
location.latitudenumberYes, in the point formThe site point
location.longitudenumberYes, in the point formThe site point
location.radiusMetersintegerNo2011 to 1,609The boundary radius in metres
location.boundaryarrayYes, in the drawn form3 or more pointsA ring of points, open or closed. No point more than 1 mile from its centre.
location.boundary[].latnumberYesLatitude
location.boundary[].lngnumberYesLongitude
completeStatusstringNoIncompleteIncomplete, CompleteWork Order only
jobCodestringNoMax 50 charactersWork Order only. The code of the Job that the Work Order's hours count under.
equipmentCodestringNoMax 50 charactersWork Order only. Equipment of the same company and of the Work Order's division.
reportingTypestring or nullNonullOne of reporting typesJob only
reportingFrequencystring or nullNonullOne of reporting frequenciesJob only
programClosesOnstring or nullNonullYYYY-MM-DDJob only. The day the program closes.
audiencearrayNo[]Max 100 itemsMeet Point only. Pairs of division and pay class that see the Meet Point. Empty means everyone.
audience[].divisionCodestring or nullNoMax 20 charactersA division. Null means any.
audience[].payClassCodestring or nullNoMax 30 charactersA pay class. Null means any. A pair must name at least one of the two.

Example request: a Job

curl -X POST "$TIMECARD_BASE_URL/activities" \
  -H "X-API-Key: $TIMECARD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "Job",
    "companyCode": "ACME",
    "code": "J-2026-104",
    "name": "Riverside Bridge Widening",
    "address": "1200 River Rd, Austin, TX 78701",
    "divisionCode": "PAVING",
    "timeZone": "America/Chicago",
    "phaseCodes": [
      { "code": "01-100", "description": "Mobilization" },
      { "code": "02-200", "description": "Earthwork" }
    ],
    "location": { "latitude": 30.2672, "longitude": -97.7431, "radiusMeters": 201 }
  }'

Example request: a Job with a drawn boundary (needs geofences:write)

curl -X POST "$TIMECARD_BASE_URL/activities" \
  -H "X-API-Key: $TIMECARD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "Job",
    "companyCode": "ACME",
    "code": "J-2026-105",
    "name": "Eastside Yard Expansion",
    "divisionCode": "PAVING",
    "timeZone": "America/Chicago",
    "location": {
      "boundary": [
        { "lat": 30.2701, "lng": -97.7460 },
        { "lat": 30.2701, "lng": -97.7400 },
        { "lat": 30.2650, "lng": -97.7400 },
        { "lat": 30.2650, "lng": -97.7460 }
      ]
    }
  }'

Example request: a Work Order

The Job and the equipment must exist first. An unknown jobCode or equipmentCode gets 400.

curl -X POST "$TIMECARD_BASE_URL/activities" \
  -H "X-API-Key: $TIMECARD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "WorkOrder",
    "companyCode": "ACME",
    "code": "WO-55123",
    "name": "Replace hydraulic hose - EX-210",
    "divisionCode": "PAVING",
    "jobCode": "J-2026-104",
    "equipmentCode": "EX-210",
    "completeStatus": "Incomplete"
  }'

Example request: a Meet Point

curl -X POST "$TIMECARD_BASE_URL/activities" \
  -H "X-API-Key: $TIMECARD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "MeetPoint",
    "companyCode": "ACME",
    "code": "YARD-NORTH",
    "name": "North Yard",
    "address": "800 Industrial Blvd, Austin, TX 78745",
    "timeZone": "America/Chicago",
    "phaseCodes": [{ "code": "YARD", "description": "Yard time" }],
    "location": { "latitude": 30.2101, "longitude": -97.7702, "radiusMeters": 150 },
    "audience": []
  }'

Example response 201 Created

{
  "status": 201,
  "message": "Success: activity created.",
  "record": {
    "activityId": 48213,
    "kind": "Job",
    "companyCode": "ACME",
    "sourceSystem": "acme-erp",
    "code": "J-2026-104",
    "name": "Riverside Bridge Widening",
    "phaseCodes": [
      { "code": "01-100", "description": "Mobilization" },
      { "code": "02-200", "description": "Earthwork" }
    ],
    "geofences": [
      {
        "geofenceId": 9120,
        "isActive": true,
        "isPublished": false,
        "hasDraftPending": true,
        "sourceSystem": "acme-erp",
        "isLocallyManaged": false
      }
    ],
    "updatedAt": "2026-11-03T15:04:05.123Z"
  }
}

The record has every field of the Activity object. It is shortened here.

Errors

StatuscodeWhen
400validation_failedThe body is not valid: for example kind is missing, a Meet Point has no Phase Code, a Work Order field is sent on another kind, timeZone is unknown, the location is not plausible, jobCode or equipmentCode names nothing in the company, or companyCode is missing when the key covers several companies
403insufficient_scopeThe key does not have activity:write
403forbiddenThe key names a company other than its own, sends a drawn boundary without geofences:write, or has no source system
404not_foundcompanyCode names no active company that the key can reach
409conflictThe code is already used in the company, by any kind and owner, active or not. detail names the kind and the owner.
500internal_errorThe activity could not be created. The record can still exist. Look it up by its code before you retry.

PATCH /activities/{activityId}

Changes only the fields that you send. code and kind cannot change. Send null to clear an optional field. Your key can change only an activity that its own source system owns.

Scope: activity:write. location also needs geofences:write, for a point and radius too.

Path parameters

NameTypeRequiredDefaultAllowed valuesDescription
activityIdintegerYes1 or moreThe activity's id

Request body

Send at least one field. An empty body gets 400.

NameTypeRequiredDefaultAllowed valuesDescription
expectedUpdatedAtstringNo (recommended)ISO 8601 date-time with Z or an offsetThe updatedAt that you last read. If the activity changed after that, you get 409 and nothing is saved.
namestringNoMax 100 charactersThe name
addressstring or nullNoMax 300 charactersThe site address
divisionCodestring or nullNoMax 20 charactersThe division
timeZonestring or nullNoMax 64 characters, IANA nameNull clears it
isActivebooleanNofalse closes the activity. true opens it again.
phaseCodesarrayNoMax 1,000 itemsReplaces the whole list. A code that you leave out is deactivated, never deleted. A code that you send again is reactivated. A Meet Point must keep at least one. Not allowed on a Work Order.
phaseCodes[].codestringYesMax 50 charactersTrimmed only
phaseCodes[].descriptionstring or nullNoMax 200 charactersThe description
locationobjectNoRedraws a boundary as a new version. Send a point and radius, or a drawn boundary, as on POST /activities.
location.latitudenumberYes, in the point formThe site point
location.longitudenumberYes, in the point formThe site point
location.radiusMetersintegerNo2011 to 1,609The boundary radius in metres
location.boundaryarrayYes, in the drawn form3 or more pointsA ring of { "lat", "lng" } points. No point more than 1 mile from its centre.
location.boundary[].latnumberYesLatitude
location.boundary[].lngnumberYesLongitude
location.geofenceIdintegerNo1 or moreThe boundary to redraw. Required when the activity has more than one boundary. Not with newFence.
location.newFencebooleanNotrueAdds a separate boundary owned by your key instead of redrawing one. Not with geofenceId.
pointobject or nullNoSets the site point only, with no boundary change and no extra scope. Null clears it. Not with a point-form location.
point.latitudenumberYesLatitude
point.longitudenumberYesLongitude
completeStatusstringNoIncomplete, CompleteWork Order only
jobCodestring or nullNoMax 50 charactersWork Order only. Null removes the Job link.
equipmentCodestring or nullNoMax 50 charactersWork Order only. Null removes the equipment link.
reportingTypestring or nullNoOne of reporting typesJob only. Null clears it.
reportingFrequencystring or nullNoOne of reporting frequenciesJob only. Null clears it.
programClosesOnstring or nullNoYYYY-MM-DDJob only. Null clears it.
audiencearrayNoMax 100 itemsMeet Point only. Replaces the whole audience. [] means everyone.
audience[].divisionCodestring or nullNoMax 20 charactersA division
audience[].payClassCodestring or nullNoMax 30 charactersA pay class

The body can also hold revert, but only an administrator can use it. A key that sends revert gets 400.

Which boundary location redraws

The activity hasYou sendResult
No boundarylocationA first boundary is added
One boundary that your key ownslocationThat boundary is redrawn
Several boundaries, or one that another owner holdslocation with no geofenceId409 conflict
Anylocation.geofenceIdThat boundary is redrawn, if your key owns it and it is not locally managed
Anylocation.newFence: trueA separate boundary is added

Without geofences:publish, a redrawn boundary is saved as a draft version (hasDraftPending: true) until an administrator publishes it.

Example request

curl -X PATCH "$TIMECARD_BASE_URL/activities/48213" \
  -H "X-API-Key: $TIMECARD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "expectedUpdatedAt": "2026-11-03T15:04:05.123Z",
    "name": "Riverside Bridge Widening - Phase 2",
    "address": "1250 River Rd, Austin, TX 78701"
  }'

Example response 200 OK

{
  "status": 200,
  "message": "Success: activity updated.",
  "record": {
    "activityId": 48213,
    "kind": "Job",
    "code": "J-2026-104",
    "name": "Riverside Bridge Widening - Phase 2",
    "address": "1250 River Rd, Austin, TX 78701",
    "updatedAt": "2026-11-03T15:10:42.517Z"
  }
}

The record has every field of the Activity object. It is shortened here. When the values you send are already stored, nothing changes and the message is Success: nothing changed.

Errors

StatuscodeWhen
400validation_failedThe body is empty or not valid, sends code or kind, sends a field that the activity's kind does not have, carries a location that is not plausible or reaches more than 1 mile, redraws a boundary that is no longer active, or jobCode or equipmentCode names nothing in the company
403insufficient_scopeThe key does not have activity:write
403forbiddenlocation is sent without geofences:write, or the key has no source system
404not_foundNo activity has this id within the key's companies, or location.geofenceId is not one of its boundaries
409conflictThe activity belongs to another source system. Or expectedUpdatedAt does not match (the activity changed after you read it). Or location has no geofenceId and the activity has several boundaries or one that another owner holds. Or the boundary is locally managed or owned by another source system.
500internal_errorThe activity could not be updated

POST /activities/import

Creates or updates up to 1,000 rows in one request. Use it for a first load or a nightly sync.

Scope: activity:write. Rows with a boundary also need geofences:write, or the whole import gets 403.

How rows become activities

  • One row per Phase Code. Rows with the same code make one activity.
  • The first row of a group gives the kind, name, address, division, time zone, completeStatus and jobCode. A later row with a different name, address or division is a warning. A later row of another kind is an error.
  • Each group's Phase Codes replace the activity's list. A Job or Work Order group with no phaseCode leaves the list as it is.
  • Each group is saved on its own. One bad group does not stop the others.
  • An existing activity is updated only when your source system owns it and it is the same kind.
  • An activity of the same kind that another source system owns is never changed. If every value that the group sends is equal to what Matrix TimeCard shows, the group counts as unchanged. If a value is different, the row is an error that names the owner and the fields. A field that the group does not send is not compared.
  • Any other holder of the code is a row error.
  • Boundaries: import adds a boundary only to an activity that has none. It never moves an existing boundary. To move one, send location on PATCH /activities/{activityId}. A location that is not usable is a warning, and the activity is imported without it.
  • A group with more than 1,000 Phase Codes or more than 100 distinct locations is a row error.
  • Send "dryRun": true first. It checks every row, reports what would happen and saves nothing.

Request body

NameTypeRequiredDefaultAllowed valuesDescription
companyCodestringNoMax 10 charactersThe company. Required when the key covers more than one company.
dryRunbooleanNofalsetrue checks every row and saves nothing
rowsarrayYes1 to 1,000 itemsThe rows
rows[].kindstringYesJob, WorkOrder, MeetPointThe kind
rows[].codestringYesMax 50 charactersThe activity's code
rows[].namestringYesMax 100 charactersThe name
rows[].divisionCodestringNoMax 20 charactersThe division
rows[].phaseCodestringNo. Required on a Meet Point row.Max 50 charactersOne Phase Code
rows[].phaseDescriptionstringNoMax 200 charactersThe Phase Code's description
rows[].addressstringNoMax 300 charactersThe site address
rows[].latitudenumberNoThe site point
rows[].longitudenumberNoThe site point
rows[].radiusMetersintegerNo2011 to 1,609The boundary radius in metres
rows[].boundaryarrayNo3 or more pointsA drawn boundary of { "lat", "lng" } points. Wins over latitude and longitude.
rows[].boundary[].latnumberLatitude
rows[].boundary[].lngnumberLongitude
rows[].timeZonestringNoMax 64 characters, IANA nameThe time zone
rows[].completeStatusstringNoIncomplete, CompleteWork Order only
rows[].jobCodestringNoMax 50 charactersWork Order only
rows[].equipmentCodestring or nullNoMax 50 charactersWork Order only. Null clears it.
rows[].audienceDivisionCodestring or nullNoMax 20 charactersMeet Point only. The division side of one audience pair. Blank or null means any.
rows[].audiencePayClassCodestring or nullNoMax 30 charactersMeet Point only. The pay class side of one audience pair.

Example request

curl -X POST "$TIMECARD_BASE_URL/activities/import" \
  -H "X-API-Key: $TIMECARD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "companyCode": "ACME",
    "dryRun": true,
    "rows": [
      {
        "kind": "Job", "code": "J-2026-110", "name": "Hwy 71 Overlay",
        "divisionCode": "PAVING", "address": "Hwy 71 & FM 973, Austin, TX",
        "latitude": 30.2033, "longitude": -97.6681, "radiusMeters": 400,
        "timeZone": "America/Chicago",
        "phaseCode": "01-100", "phaseDescription": "Mobilization"
      },
      {
        "kind": "Job", "code": "J-2026-110", "name": "Hwy 71 Overlay",
        "phaseCode": "04-400", "phaseDescription": "Asphalt overlay"
      }
    ]
  }'

Example response 200 OK

{
  "status": 200,
  "message": "Success: dry run complete. Nothing was written.",
  "record": {
    "dryRun": true,
    "created": 1,
    "updated": 0,
    "unchanged": 0,
    "phaseCodesAdded": 2,
    "fencesCreated": 1,
    "errors": [],
    "warnings": []
  }
}

Without dryRun, the message is Success: activity import complete.

Response fields

FieldTypeDescription
record.dryRunbooleanTrue when nothing was saved
record.createdintegerActivities created (or that would be, on a dry run)
record.updatedintegerActivities updated
record.unchangedintegerActivities that already held these values
record.phaseCodesAddedintegerPhase Codes added
record.fencesCreatedintegerBoundaries added
record.errors[].rowintegerThe row's position in rows, starting at 1
record.errors[].messagestringWhy the row's activity was refused
record.warnings[].rowintegerThe row's position in rows, starting at 1
record.warnings[].messagestringWhat was doubted or skipped

Errors

StatuscodeWhen
400validation_failedThe body is not JSON, rows is empty or has more than 1,000 items, or companyCode is missing when the key covers several companies. A bad row is reported in errors, not here.
403insufficient_scopeThe key does not have activity:write
403forbiddenThe key names another company, rows carry a boundary without geofences:write, or the key has no source system
404not_foundcompanyCode names no active company that the key can reach
500internal_errorThe import could not run