---
name: matrix-timecard-api
description: Use this skill when you integrate with or call the Matrix TimeCard API to create, read or update Jobs, Work Orders, Meet Points, Phase Codes, boundaries, equipment or proposed changes. It gives the base URL, the API key rules, the safe-update rules and one curl recipe for each common task.
---

# Matrix TimeCard API

The Matrix TimeCard API lets a company system write the Jobs, Phase Codes, Work Orders,
Meet Points and equipment that crews clock in to. The phone app shows them and checks every
punch against them. This skill restates the public docs. When the skill and the docs
disagree, the docs are correct. The full docs are listed at the end.

## Base URL

Each Matrix TimeCard account has its own API address, in this form:

```
https://<your-instance-host>/api/v1
```

- Every path below comes after `/api/v1`.
- Ask the account administrator for the host, or support@matrixtimecard.com. **Never guess a host.**
- Read the base URL and the key from environment variables. Do not write them in code.

```bash
export TIMECARD_BASE_URL="https://<your-instance-host>/api/v1"
export TIMECARD_API_KEY="tc_live_..."
```

## Authentication

- Send the key in the `X-API-Key` header. `Authorization: Bearer <key>` also works. If you
  send both, the API reads `Authorization`.
- A key starts with `tc_live_` or `tc_test_`, then 43 letters and digits. Send the whole key.
- **A `tc_test_` key writes live data.** There is no sandbox. Use a test account.
- Never put the key in a URL or a query string. Never log the key or the request headers.
  Never print the key in your output.
- A key bound to one company can leave `companyCode` out. A key that covers every company
  must send `companyCode` on a create when the account has more than one company.

| Scope                     | Lets the key                                                         |
| ------------------------- | -------------------------------------------------------------------- |
| `activity:read`           | Read Jobs, Work Orders, Meet Points, Phase Codes and reference lists |
| `activity:write`          | Create and update Jobs, Work Orders, Meet Points and Phase Codes     |
| `geofences:write`         | Draw, redraw, or turn a boundary off or on                           |
| `geofences:publish`       | Make a drawn boundary live at once (else it waits as a draft)        |
| `equipment:read`          | Read equipment                                                       |
| `equipment:write`         | Create, update, deactivate and reactivate equipment                  |
| `reference-changes:write` | Send proposed changes for an administrator to approve                |

A missing scope gets `403 insufficient_scope`, and `required_scopes` names the scope.

## Rules you must follow

1. **The server is the authority.** Do not decide in your code that a value is valid. Send
   it and read the response.
2. **Nothing is deleted.** To retire a Job, Work Order, Meet Point, Phase Code or boundary,
   send `"isActive": false`. `DELETE /equipment-catalog/{equipmentId}` deactivates
   equipment, and `POST /equipment-catalog/{equipmentId}/reactivate` undoes it.
3. **Codes.** Job, Work Order and Meet Point codes are stored trimmed and in upper case.
   Compare them in upper case. A code never changes after the record is created.
4. **Ownership.** Each record has an owner in `sourceSystem`. Your key can change only the
   records that its own source system owns. A write to another owner's record gets
   `409 conflict`, or `403 not_owner` for equipment. Do not try to work around it.
5. **Safe updates.** On every `PATCH`, send `expectedUpdatedAt` with the `updatedAt` that
   you last read. On `409 conflict`, read the record again, apply your change to the new
   values, and send it one more time. If that also fails, stop and tell the human.
6. **Phase Code lists.** `phaseCodes` in `PATCH /activities/{activityId}` **replaces the
   whole list**. A code that you leave out is deactivated. To add one code, use
   `POST /activities/{activityId}/phase-codes`.
7. **A 500 on a create can still save the record.** Before you send the create again, look
   the record up by its code. A create sent again for a saved record gets `409`.
8. **Paging.** Lists return `record.records` and `record.total`. Send `limit` and `offset`.
   Read pages until `offset` is equal to or more than `total`. The `q` search matches part
   of a code, name or address, so a lookup by code must read every page and compare `code`.
9. **Retries.** On `429` or `503`, wait and retry with exponential back-off (1 s, 2 s,
   4 s), and stop after 5 tries. Never retry a `400`, `401`, `403`, `404` or `409`
   unchanged. A `PATCH` that sends values the record already has changes nothing, so it is
   safe to retry.
10. **Dry run first.** For `POST /activities/import`, `POST /phase-codes/apply` and
    `POST /reference-changes`, send `"dryRun": true` first, show the result to the human,
    and send the real request only after they agree.
11. **Ask the human first** before a bulk or destructive change: closing Jobs, deactivating
    equipment or Phase Codes, an import, or `"allActiveJobs": true`.

## Recipes

### Check the key

`GET /reporting-types` reads no account data. It needs `activity:read`. A `200` means the
base URL and the key work.

```bash
curl "$TIMECARD_BASE_URL/reporting-types" -H "X-API-Key: $TIMECARD_API_KEY"
```

### Create a Job

`POST /activities` creates the Job, its Phase Codes and a boundary around the point. Either
all of it is saved or nothing is. Keep `activityId` and `updatedAt` from `record`.

```bash
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", "timeZone": "America/Chicago",
       "phaseCodes": [{"code": "01-100", "description": "Mobilization"}],
       "location": {"latitude": 30.2672, "longitude": -97.7431, "radiusMeters": 201}}'
```

### Find a Job by its code

Read every page (up to 100 records a page) and pick the record whose `code` is equal to
your code, in upper case.

```bash
curl "$TIMECARD_BASE_URL/activities?kind=Job&q=J-2026-104&companyCode=ACME&limit=100&offset=0" \
  -H "X-API-Key: $TIMECARD_API_KEY"
```

### Update a Job safely

`PATCH /activities/{activityId}` changes only the fields that you send. First read the Job
with `GET /activities/{activityId}` to get its `updatedAt`.

```bash
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"}'
```

### Add a Phase Code

This adds one code and does not touch the others. It moves the Job's `updatedAt`.

```bash
curl -X POST "$TIMECARD_BASE_URL/activities/48213/phase-codes" \
  -H "X-API-Key: $TIMECARD_API_KEY" -H "Content-Type: application/json" \
  -d '{"code": "03-300", "description": "Concrete"}'
```

### Close a Job

Ask the human first. Crews can no longer clock in to a closed Job. Send the latest
`updatedAt`. `"isActive": true` opens the Job again.

```bash
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:12:08.004Z", "isActive": false}'
```

### Create or update equipment

Look the code up first with `GET /equipment-catalog?q=<code>`. If it exists, update it.
`POST /equipment-catalog` on a used code gets `409 code_taken`.

```bash
curl -X POST "$TIMECARD_BASE_URL/equipment-catalog" \
  -H "X-API-Key: $TIMECARD_API_KEY" -H "Content-Type: application/json" \
  -d '{"companyCode": "ACME", "code": "EX-210", "description": "CAT 320 excavator", "status": "A"}'

curl -X PATCH "$TIMECARD_BASE_URL/equipment-catalog/3051" \
  -H "X-API-Key: $TIMECARD_API_KEY" -H "Content-Type: application/json" \
  -d '{"expectedUpdatedAt": "2026-11-03T15:00:12.000Z", "status": "D"}'
```

### Propose changes

Use `POST /reference-changes` when the company wants a person to approve each change. Send
up to 1,000 records. Do not mix proposals and direct writes for the same codes. Send a dry
run first, then the same body with `"dryRun": false` after the human agrees.

```bash
curl -X POST "$TIMECARD_BASE_URL/reference-changes" \
  -H "X-API-Key: $TIMECARD_API_KEY" -H "Content-Type: application/json" \
  -d '{"companyCode": "ACME", "dryRun": true, "changes": [
        {"entity": "job", "jobCode": "J-2026-120", "name": "Lakeline Parking Lot"},
        {"entity": "phase_code", "jobCode": "J-2026-120", "phaseCode": "01-100"}]}'
```

Each result has an `outcome`: `staged`, `published`, `matches_current`, `unchanged` or
`refused`. Show refused records and their `message` to the human.

## Errors

A failed request returns a problem body. Write your handling against `code`. Do not parse
`detail` or `message`.

| HTTP | `code`                                     | What to do                                                        |
| ---- | ------------------------------------------ | ----------------------------------------------------------------- |
| 400  | `validation_failed`                        | Read `detail`, fix the request. Do not retry it unchanged.        |
| 401  | none, or an `api_key_*` code               | Stop. Tell the human that the key is missing or not valid.        |
| 403  | `insufficient_scope`                       | Stop. Tell the human which scope `required_scopes` names.         |
| 403  | `forbidden`, `not_owner`                   | Check `companyCode` and the owner. Do not write others' records.  |
| 404  | `not_found`                                | Check the id. A record outside the key's companies is also `404`. |
| 409  | `conflict`                                 | Read `detail`. For a stale update, read again and retry one time. |
| 409  | `code_taken`                               | Equipment code is used. Update the existing record instead.       |
| 429  | `rate_limited`                             | Back off and retry, max 5 tries.                                  |
| 500  | `internal_error`                           | Before you retry a create, look the record up by its code.        |
| 503  | `service_unavailable`, `instance_mismatch` | Back off and retry. If it continues, tell the human.              |

## Full reference

Each page is plain Markdown. Read the page before you use an endpoint that this skill does
not show.

- [Overview](https://matrixtimecard.com/docs/README.md): what the API is, the base URL and the words it uses
- [Getting started](https://matrixtimecard.com/docs/getting-started.md): the first call and a full Job sync
- [Authentication](https://matrixtimecard.com/docs/authentication.md): keys, scopes, company access, source system
- [Errors](https://matrixtimecard.com/docs/errors.md): every error code and the retry rules
- [Conventions](https://matrixtimecard.com/docs/conventions.md): responses, paging, codes, ownership, safe updates
- [Activities](https://matrixtimecard.com/docs/reference/activities.md): Jobs, Work Orders, Meet Points and import
- [Phase Codes](https://matrixtimecard.com/docs/reference/phase-codes.md): one activity, or many Jobs at once
- [Boundaries](https://matrixtimecard.com/docs/reference/boundaries.md): turn a boundary off or on
- [Equipment](https://matrixtimecard.com/docs/reference/equipment.md): create, update, deactivate, reactivate
- [Proposed changes](https://matrixtimecard.com/docs/reference/proposed-changes.md): changes an administrator approves
- [Reference lists](https://matrixtimecard.com/docs/reference/reference-lists.md): the fixed values some fields take
- [Terms and limits](https://matrixtimecard.com/docs/terms-and-limits.md): rate, size and field limits
- [Index of every page](https://matrixtimecard.com/llms.txt)
