# Job details, tags & share links

Everything here runs the job page's own actions as the person who created the API key, so the same rules apply: a member who only sees their own jobs can only change those, owners and schedulers set divisions, and every change lands in the job's activity log under that person.

**Share and review links are never sent by TruTec through these endpoints:** each call returns a link for you to send. The one endpoint here that reaches a customer directly is the [portal reply](#reply-in-the-customer-portal): the customer sees it in their portal.

!!! note
    Partner keys that act for several workspaces must also send `firm_id` (query string, or JSON body on writes).

## Job details

`PATCH /v1/jobs/{job_id}` covers the sales fields (name, contact, address, value, dates, stage...). These cover the rest of the job page.

### Get job details

`GET /v1/jobs/{job_id}/details`

**Permission:** Read jobs

```json
{
  "data": {
    "job_id": 70412,
    "job_number": 1034,
    "display_job_number": "J-1034",
    "accounting_job_number": "26-118",
    "property_reference": "PR-4471",
    "own_property_reference": null,
    "external_work_order_number": "WO-88120",
    "description_customer_visible": false,
    "bid_due_date": "2026-10-30",
    "customer_type": "Property management",
    "property_type": "Retail",
    "bond_required": false,
    "liquidated_damages": true,
    "liquidated_damages_per_day": 500.0,
    "davis_bacon": null,
    "linked_contact_id": 5512,
    "location_id": 812,
    "location": {"id": 812, "name": "Oak Hills Plaza", "address": "1200 Commerce Dr, Austin, TX"},
    "tags": [{"id": 14, "name": "HOA", "color": "#2563eb"}],
    "operations": {
      "division": "seal", "division_name": "Sealcoating",
      "division_options": [{"key": "seal", "name": "Sealcoating"}, {"key": "paving", "name": "Paving"}],
      "assignee_user_id": 118, "assignee": {"id": 118, "name": "Pat Rivera"}
    }
  }
}
```

`property_reference` is the job's own Property ID, else its site's (`own_property_reference` is only what was typed on the job). `bond_required`, `liquidated_damages` and `davis_bacon` are `null` until someone answers. `operations` is `null` when crew scheduling is off.

### Update job details

`PATCH /v1/jobs/{job_id}/details`

**Permission:** Write jobs

Only the fields sent change.

| Name | Type | Description |
|---|---|---|
| `job_number` | integer | Renumber this one job. Must be unused in the workspace (`400` names the job that has it). New jobs keep counting up from the highest number. |
| `accounting_job_number` | string or null | Up to 80 characters. |
| `property_reference` | string or null | The customer's Property ID for this job. Blank or null goes back to the site's. |
| `external_work_order_number` | string or null | The customer's work order number. |
| `description_customer_visible` | boolean | Show the job description to the customer. |
| `bid_due_date` | string or null | YYYY-MM-DD. |
| `customer_type`, `property_type` | string or null | |
| `bond_required`, `liquidated_damages`, `davis_bacon` | boolean or null | |
| `liquidated_damages_per_day` | number or null | Dollars. |
| `linked_contact_id` | integer | Makes this saved contact the job's main contact (fills the job's contact fields). To take someone off the job use `DELETE /v1/jobs/{job_id}/contacts/{contact_id}`. |

Returns the details. **Errors:** `400` with the app's reason (for example a job number already used, or a contact from another customer); `404` for a job or contact outside the workspace.

### Division and operations assignee

`PATCH /v1/jobs/{job_id}/operations-assignment`

**Permission:** Write jobs. The key's user must be an owner or able to edit the schedule.

For workspaces with crew scheduling (`403` otherwise).

| Name | Type | Description |
|---|---|---|
| `division` | string or null | A key from `division_options`; null = follows the service. |
| `operations_assignee_user_id` | integer or null | Who runs the job on the operations side; null clears. |

### Set the job's site

`PUT /v1/jobs/{job_id}/location`

**Permission:** Write jobs

| Body | What it does |
|---|---|
| `{"location_id": 812}` | Link the job to that site. A job with no address takes the site's; a job with no company is filed under the site's company. |
| `{"create_from_address": true}` | Save the job's address as a site (or link the site that already has it). The job needs an address. |
| `{"location_id": null}` | Take the job off its site. |

Returns `{"data": {"job_id", "location_id", "location", "client_account_id"}}`.

## Tags

| Endpoint | Permission | What it does |
|---|---|---|
| `GET /v1/job-tags` | Read jobs | The workspace's tags (`q` searches names). |
| `POST /v1/job-tags` | Write jobs | Add a tag: `name` (up to 50 characters), `color` (`#rrggbb`). `409` with the existing tag when the name is taken. |
| `POST /v1/jobs/{job_id}/tags` | Write jobs | Tag a job: `{"tag_ids": [14, 22]}`. Returns the job's tags. |
| `DELETE /v1/jobs/{job_id}/tags/{tag_id}` | Write jobs | Take a tag off the job (the tag stays). `204`. |

## Customer portal conversation

The conversation on a job in the customer's portal: their questions and the project team's replies, including comments the customer pinned on the map.

### List portal messages

`GET /v1/jobs/{job_id}/portal-messages`

**Permission:** Read jobs

Oldest first; `limit` keeps the most recent N (default 100, max 200). Each message has `id`, `body`, `author_type` (`customer` or `staff`), `author_name`, `created_at`, and for map comments `map_comment`, `latitude`, `longitude`, `location_id`, `location_name`.

### Reply in the customer portal

`POST /v1/jobs/{job_id}/portal-messages`

**Permission:** Write jobs

!!! warning
    **The customer sees this reply** in their portal, signed by the key's user. TruTec doesn't email or text it, but treat it as a message to the customer.

`{"body": "We're on site Thursday at 7 AM."}` (up to 2,000 characters). Returns `201` with the message.

## Review links

| Endpoint | Permission | What it does |
|---|---|---|
| `POST /v1/jobs/{job_id}/review-request` | Write jobs | Make a review link for the job's customer (`client_name` and `client_email` default from the job). `201` with `review_url`. **Nothing is sent.** |
| `GET /v1/reviews` | Read jobs | Review links and written reviews, newest first. Filter `job_id`, `submitted` (true = written, false = still waiting). |

A review has `id`, `job_id`, `client_name`, `client_email`, `submitted`, `rating` (1-5), `content`, `is_public`, `review_url`, `submitted_at`, `created_at`.

## Photo links

### The job's photo report link

One public link per job showing its customer-visible photos marked for the report.

| Endpoint | Permission | What it does |
|---|---|---|
| `GET /v1/jobs/{job_id}/photo-share-link` | Read jobs | The link and its settings (`enabled: false` until one is made). |
| `POST /v1/jobs/{job_id}/photo-share-link` | Write jobs | Make it (`201`) or return the existing one (`200`). Optional `access_code`, `expires_in_days`. |
| `PATCH /v1/jobs/{job_id}/photo-share-link` | Write jobs | `access_code` (4-64 characters) protects it, `public: true` removes the code, `expires_in_days` (1-365, or null for never). |

```json
{"data": {"job_id": 70412, "enabled": true, "share_url": "https://trutec.ai/photos/share/3f9a1c22b0de",
          "access_mode": "code", "has_access_code": true, "expires_at": "2026-11-12T15:02:10Z", "is_expired": false}}
```

### Links to chosen photos

A link to exactly the photos you pick (customer-visible photos on the job), for 7, 30 or 90 days. Photos added to the job later never appear in it.

| Endpoint | Permission | What it does |
|---|---|---|
| `GET /v1/jobs/{job_id}/photo-selection-shares` | Read jobs | The job's chosen-photo links, newest first. |
| `POST /v1/jobs/{job_id}/photo-selection-shares` | Write jobs | `photo_ids` (required), `title`, `expires_in_days` (7, 30 or 90; default 30). `201`. A photo that is internal or on another job is a `400`. |
| `POST /v1/jobs/{job_id}/photo-selection-shares/{share_id}/revoke` | Write jobs | Turn it off for good. |

## Assessment report links

`POST /v1/site-assessments/{assessment_id}/share-link`

**Permission:** Write jobs

The site assessment report's public link. Send `service_type_ids` (services that have pins in the assessment) to make a separate link showing only those services' pins, for a sub bidding part of the work; each such call makes a new link, optionally named with `label`. Returns `{"data": {"assessment_id", "share_url", "scoped", "service_type_ids", "label"}}`.

## Capital plan links

A read-only page of a site's multi-year capital plan to send the property manager. For workspaces with capital planning (`403` otherwise).

| Endpoint | What it does |
|---|---|
| `POST /v1/locations/{location_id}/plan-share-link` | Turn the link on (or return it). |
| `POST /v1/locations/{location_id}/plan-share-link/disable` | Turn it off; the old link stops working. |
| `POST /v1/locations/{location_id}/plan-share-link/rotate` | A new link; the old one stops working. |

**Permission:** Write jobs. Each returns `{"data": {"location_id", "enabled", "share_url"}}`.
