# Collab Integration — HubSpot CRM (deployment curls)

TW-331 HubSpot CRM tools — same contract as Google Drive collab.

**Postman:** Collab Integration — HubSpot CRM  
Workspace collection UID: `51852693-865dc619-eabb-4885-8964-d7a45cec1559`

---

## Contract

| | |
|---|---|
| Method / path | `POST /internal/v1/tools/{tool_name}` |
| Local | `http://localhost:6010` |
| Prod | `https://adhoc-mcp.lucos.com` |
| Headers | `Content-Type: application/json`, `X-Lucos-Org-Id` (required). Prod may also need `Authorization: Bearer <COLLAB_S2S_TOKEN>` |
| Body | `{ "environment": "staging" \| "prod", "args": { ... } }` |

Pagination: pass `"after": "<next_after from previous response>"` in `args` **and** `account` set to that portal’s slug. All-accounts responses cannot be paginated with a single cursor (each portal has its own `by_account[].next_after`).  
Limit default **50**, max **200**. Omitting `account` (or `account=all`) searches **all** token-configured portals; each portal uses the same `limit` (not split).

---

## Match HubSpot UI (important)

Search/get results are shaped to match HubSpot CRM list views for India viewers:

| Behavior | Default |
|----------|---------|
| Timezone for day filters + `*_local` dates | **`Asia/Kolkata` (GMT+5:30)** — same as HubSpot UI columns |
| `last_activity_on` / `created_on` / `last_activity_week` | Calendar day in that timezone |
| Sort when using those day/activity filters | **Create Date ↓** (matches HubSpot “All companies/contacts” when Create Date is sorted) |
| Deal `month=current` (close date) | Sorted by **Close Date ↑** |
| Owners | Resolved names: `contact_owner` / `company_owner` / `deal_owner` / `ticket_owner` |
| Response `ui` | `{ timezone, sort, object_type, columns }` |

**Do not use portal account TZ (US/Eastern) for “yesterday”** if you want to match the HubSpot UI that shows GMT+5:30 — that yields a different company/contact set and count.

Override timezone only when intentional: `"timezone": "US/Eastern"` or `use_portal_timezone: true`.

---

## Server env (do not put tokens in curls)

```bash
HUBSPOT_ACCOUNTS_FILE=./config/hubspot-accounts.json
HUBSPOT_TOKEN_INFLUENTIAL=...
HUBSPOT_TOKEN_ADVERTISING=...
HUBSPOT_TOKEN_ADCOM=...
HUBSPOT_TOKEN_MONETIZE=...

# Match HubSpot UI GMT+5:30 for India viewers (recommended)
HUBSPOT_DATE_TIMEZONE=Asia/Kolkata

# Approval-gated note writes (off by default)
# HUBSPOT_WRITES_ENABLED=false
# HUBSPOT_WRITE_APPROVAL_TTL_SEC=600
# Pending propose→confirm tokens are in-memory (TTL); no SQLite.
```

### Accounts

| `account` slug | Label | Portal ID | Token env | Portal account TZ* |
|---|---|---|---|---|
| `influential` | Influential.com | 245231031 | `HUBSPOT_TOKEN_INFLUENTIAL` | US/Eastern |
| `advertising` | Advertising.com | 244656364 | `HUBSPOT_TOKEN_ADVERTISING` | US/Eastern |
| `adcom` | Ad.com | 44357896 | `HUBSPOT_TOKEN_ADCOM` | America/Chicago |
| `monetize` | Monetize.com | 24315290 | `HUBSPOT_TOKEN_MONETIZE` | US/Eastern |

\*Portal account TZ ≠ UI display TZ. UI date filters for India users follow **GMT+5:30**.

**Important:** Ad.com (`adcom`) ≠ Advertising.com (`advertising`).

### Multi-account reads (fan-out)

Search / list / get tools query **all four** portals when `account` is omitted or set to `all` / `*` / `all-accounts`. Pass `account` (slug, label, or portalId — including aliases like `"Ad.com"` → `adcom`) to scope to one portal.

All-accounts responses add:

| Field | Meaning |
|---|---|
| `searched_all_accounts` | `true` |
| `account` | `"all"` |
| `accounts_searched` | slugs queried |
| `by_account[]` | per-portal `ok`, `result_count`, `total`, `next_after`, `has_more`, `error` |
| `rows` | concatenated hits (each row still has its own `account` / `portal_id`) |

When answering, report counts/totals **per account** from `by_account` (`account_label` + `total` / `result_count`), including portals with zero matches.

- Pagination still needs a specific portal: `account=<slug>` plus that portal’s `by_account[].next_after`. Passing `after` without a specific account returns `invalid_argument`.
- Writes (`hubspot_propose_create_note`) still require `account`. Do not fan out writes.
- `slack_channel_id` does **not** pin reads to one portal. Generic (no account arg) = all portals.

### Private App scopes (all portals)

Grant the **same** scope set on **every** Private App (Influential, Advertising, Ad.com, Monetize). Activities / BD leaderboards need engagement reads on each portal:

| Scope | Used by |
|---|---|
| `crm.objects.contacts.read` / `companies` / `deals` / `tickets` | Search + get |
| `crm.objects.owners.read` | Owner names / BD rep leaderboard |
| `crm.objects.notes.read` | Notes |
| `crm.objects.emails.read` | Emails (**required for type=all / email column**) |
| `crm.objects.calls.read` | Calls |
| `crm.objects.meetings.read` | Meetings |
| `crm.objects.tasks.read` | Tasks |

How: each portal → **Settings → Integrations → Private Apps** → your collab app → **Scopes** → add any missing from the table → Save (token stays the same).

| Account | Engagement scopes status (verified) |
|---|---|
| `influential` | notes + emails + calls + meetings + tasks OK |
| `advertising` | notes + emails + calls + meetings + tasks OK |
| `adcom` | notes + emails + calls + meetings + tasks OK |
| `monetize` | notes + calls + meetings + tasks OK; **add `crm.objects.emails.read`** |

`hubspot_search_activities` with `type=all` skips engagement types that return a missing-scope error and returns partial rows + `skipped_types` instead of failing the whole call (any account).

---

## Tools to enable (MCP / gateway)

- `hubspot_list_accounts`
- `hubspot_search_contacts` / `hubspot_get_contact` / `hubspot_get_contact_related`
- `hubspot_search_companies` / `hubspot_get_company`
- `hubspot_search_deals` / `hubspot_get_deal`
- `hubspot_search_tickets` / `hubspot_get_ticket`
- `hubspot_list_owners` / `hubspot_get_owner`
- `hubspot_list_properties` / `hubspot_list_pipelines` / `hubspot_list_lists` / `hubspot_list_list_members` *(HelloMCP: enable these four for property/pipeline/list discovery)*
- `hubspot_search_notes` / `hubspot_get_note`
- `hubspot_list_associations`
- `hubspot_get_timeline`
- `hubspot_search_activities`
- `hubspot_get_attachment`
- `hubspot_propose_create_note` / `hubspot_confirm_write` / `hubspot_cancel_write`

---

## Setup (shell)

```bash
BASE=https://adhoc-mcp.lucos.com   # or http://localhost:6010
ORG=00000000-0000-0000-0000-000000000001
# Optional on prod:
# export COLLAB_S2S_TOKEN=...
# AUTH_HDR=(-H "Authorization: Bearer $COLLAB_S2S_TOKEN")
```

Add `"${AUTH_HDR[@]}"` after the org header on prod if Bearer is required.

---

## 1. Health

```bash
curl -sS "$BASE/healthz"
```

---

## 2. hubspot_list_accounts

```bash
curl -sS -X POST "$BASE/internal/v1/tools/hubspot_list_accounts" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"verify":true}}'
```

---

## 3. hubspot_search_contacts

Rows include: `name`, `email`, `contact_owner`, `company`, `createdate_local`, `last_activity_local`, `phone`.

```bash
curl -sS -X POST "$BASE/internal/v1/tools/hubspot_search_contacts" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"account":"advertising","query":"","limit":50}}'
```

### Created yesterday (UI Create Date day)

```bash
curl -sS -X POST "$BASE/internal/v1/tools/hubspot_search_contacts" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"account":"advertising","created_on":"yesterday","timezone":"Asia/Kolkata","limit":50}}'
```

### Last activity yesterday (UI Last Activity Date)

```bash
curl -sS -X POST "$BASE/internal/v1/tools/hubspot_search_contacts" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"account":"advertising","last_activity_on":"yesterday","timezone":"Asia/Kolkata","limit":50}}'
```

### Last activity this week

```bash
curl -sS -X POST "$BASE/internal/v1/tools/hubspot_search_contacts" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"account":"advertising","last_activity_week":"current","timezone":"Asia/Kolkata","limit":50}}'
```

Optional sort override: `"sort": "last_activity"` (default for these filters is Create Date ↓).

---

## 4. hubspot_get_contact

```bash
curl -sS -X POST "$BASE/internal/v1/tools/hubspot_get_contact" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"account":"advertising","contact_id":"CONTACT_ID"}}'
```

---

## 5. hubspot_get_contact_related

Deals / tickets / companies / notes / attachment ids for a contact.

```bash
curl -sS -X POST "$BASE/internal/v1/tools/hubspot_get_contact_related" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"account":"advertising","contact_id":"CONTACT_ID"}}'
```

---

## 6. hubspot_search_companies

Rows include: `name`, `company_owner`, `createdate_local`, `phone`, `last_activity_local`, `city`, `domain`.

```bash
curl -sS -X POST "$BASE/internal/v1/tools/hubspot_search_companies" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"account":"advertising","query":"","limit":50}}'
```

### All portals (omit `account`, or `account=all`)

Queries Influential, Advertising, Ad.com, and Monetize in parallel. Use `by_account` for per-portal totals. Paginate one portal with `account=<slug>` + that portal’s `next_after`.

```bash
curl -sS -X POST "$BASE/internal/v1/tools/hubspot_search_companies" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"query":"","limit":50}}'
```

### Last activity yesterday — matches HubSpot UI (Advertising.com = 44)

Filter = Last Activity Date yesterday (IST). Sort = Create Date ↓ (Y Combinator first, Nasrev second, …).

```bash
curl -sS -X POST "$BASE/internal/v1/tools/hubspot_search_companies" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"account":"advertising","last_activity_on":"yesterday","timezone":"Asia/Kolkata","limit":50}}'
```

### Last activity this week

```bash
curl -sS -X POST "$BASE/internal/v1/tools/hubspot_search_companies" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"account":"advertising","last_activity_week":"current","timezone":"Asia/Kolkata","limit":50}}'
```

---

## 7. hubspot_get_company

```bash
curl -sS -X POST "$BASE/internal/v1/tools/hubspot_get_company" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"account":"advertising","company_id":"COMPANY_ID"}}'
```

---

## 8. hubspot_search_deals

Rows include: `dealname`, `deal_owner`, `amount`, `dealstage`, `closedate_local`, `createdate_local`.

```bash
curl -sS -X POST "$BASE/internal/v1/tools/hubspot_search_deals" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"account":"advertising","limit":50}}'
```

### Close date this month (`closedate`, not create date)

Matches HubSpot UI “Close date = this month” (IST month-to-date).

```bash
curl -sS -X POST "$BASE/internal/v1/tools/hubspot_search_deals" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"account":"advertising","month":"current","date_property":"closedate","timezone":"Asia/Kolkata","limit":50}}'
```

### Created last 30 days

```bash
curl -sS -X POST "$BASE/internal/v1/tools/hubspot_search_deals" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"account":"advertising","created_within_days":30,"limit":50}}'
```

Never put `closedwon` in `query` — use `"closed_only": true` or `"dealstage": "closedwon"`.

---

## 9. hubspot_get_deal

```bash
curl -sS -X POST "$BASE/internal/v1/tools/hubspot_get_deal" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"account":"advertising","deal_id":"DEAL_ID"}}'
```

---

## 10. hubspot_search_tickets

Rows include: `subject`, `ticket_owner`, `hs_pipeline_stage`, `hs_ticket_priority`, `createdate_local`.

```bash
curl -sS -X POST "$BASE/internal/v1/tools/hubspot_search_tickets" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"account":"advertising","limit":50}}'
```

### By contact

```bash
curl -sS -X POST "$BASE/internal/v1/tools/hubspot_search_tickets" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"account":"advertising","contact_id":"CONTACT_ID","limit":50}}'
```

Empty `total=0` means none associated — not a missing tool.

---

## 11. hubspot_get_ticket

```bash
curl -sS -X POST "$BASE/internal/v1/tools/hubspot_get_ticket" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"account":"advertising","ticket_id":"TICKET_ID"}}'
```

---

## 12. hubspot_list_owners / hubspot_get_owner

```bash
curl -sS -X POST "$BASE/internal/v1/tools/hubspot_list_owners" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"account":"advertising","limit":100}}'

curl -sS -X POST "$BASE/internal/v1/tools/hubspot_get_owner" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"account":"advertising","owner_id":"OWNER_ID"}}'
```

---

## 12b. hubspot_list_properties / hubspot_list_pipelines / hubspot_list_lists / hubspot_list_list_members

Catalog helpers — call before inventing CRM Search `filters` property names or deal/ticket stage ids.

### Properties (contacts example)

```bash
curl -sS -X POST "$BASE/internal/v1/tools/hubspot_list_properties" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"account":"advertising","object_type":"contacts","query":"lifecycle","limit":50}}'
```

### Pipelines + stages (deals — pass stage **id** as `dealstage`)

```bash
curl -sS -X POST "$BASE/internal/v1/tools/hubspot_list_pipelines" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"account":"advertising","object_type":"deals"}}'
```

### Static lists

Uses `GET /crm/v3/lists` (falls back to `GET /crm/lists/2026-03` on 404).

```bash
curl -sS -X POST "$BASE/internal/v1/tools/hubspot_list_lists" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"account":"advertising","query":"newsletter","limit":50}}'
```

### List members (not a CRM Search filter — requires `list_id`)

```bash
curl -sS -X POST "$BASE/internal/v1/tools/hubspot_list_list_members" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"account":"advertising","list_id":"LIST_ID","object_type":"contacts","limit":25}}'
```

---

## 12c. CRM Search convenience filters + raw `filters`

These map onto `POST /crm/objects/2026-03/{objectType}/search` inside slack-bot (`resolveSearchFilters`). MCP/business-mcp passes them through when registered.

| Convenience arg | HubSpot property | Operator |
|---|---|---|
| `owner_id` / `hubspot_owner_id` | hubspot_owner_id | EQ |
| `email` | email | EQ (full email) or CONTAINS_TOKEN |
| `domain` | domain | EQ |
| `lifecyclestage` / `lifecycle_stage` / `lifecycle` | lifecyclestage | EQ |
| `hs_lead_status` / `lead_status` | hs_lead_status | EQ |
| `city`, `industry` | city, industry | EQ |
| `pipeline`, `hs_pipeline` | pipeline / hs_pipeline | EQ |
| `hs_pipeline_stage` / `ticket_stage` | hs_pipeline_stage | EQ |
| `amount_gte` / `amount_min`, `amount_lte` / `amount_max` | amount | GTE / LTE |
| `jobtitle` / `job_title`, `phone` | jobtitle, phone | CONTAINS_TOKEN |

`list_id` / `hs_list_id` are **not** CRM Search filters — use `hubspot_list_list_members`.

### search_contacts by owner + lifecycle

```bash
curl -sS -X POST "$BASE/internal/v1/tools/hubspot_search_contacts" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"account":"advertising","owner_id":"OWNER_ID","lifecyclestage":"customer","limit":50}}'
```

### search_companies by domain

```bash
curl -sS -X POST "$BASE/internal/v1/tools/hubspot_search_companies" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"account":"advertising","domain":"acme.com","limit":50}}'
```

### search_deals by amount + raw filters

```bash
curl -sS -X POST "$BASE/internal/v1/tools/hubspot_search_deals" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"account":"advertising","amount_gte":10000,"open_only":true,"filters":[{"propertyName":"dealtype","operator":"EQ","value":"newbusiness"}],"limit":50}}'
```

---

## 13. hubspot_search_notes / hubspot_get_note

```bash
curl -sS -X POST "$BASE/internal/v1/tools/hubspot_search_notes" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"account":"advertising","limit":50}}'

curl -sS -X POST "$BASE/internal/v1/tools/hubspot_get_note" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"account":"advertising","note_id":"NOTE_ID"}}'
```

---

## 14. hubspot_list_associations

```bash
curl -sS -X POST "$BASE/internal/v1/tools/hubspot_list_associations" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"account":"advertising","object_type":"contacts","id":"CONTACT_ID"}}'
```

Optional: `"to_object_type":"deals"` to filter.

---

## 15. hubspot_get_timeline

Unified notes/emails/calls/meetings/tasks for a record (sorted by timestamp desc). Pagination via `after` = numeric offset from `next_after`.

```bash
curl -sS -X POST "$BASE/internal/v1/tools/hubspot_get_timeline" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"account":"advertising","object_type":"contacts","id":"CONTACT_ID","limit":50}}'
```

---

## 16. hubspot_search_activities

```bash
curl -sS -X POST "$BASE/internal/v1/tools/hubspot_search_activities" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"account":"advertising","type":"notes","created_within_days":30,"limit":50}}'
```

`type`: `notes` | `emails` | `calls` | `meetings` | `tasks` | `all` (or comma list).

---

## 17. hubspot_get_attachment

Requires Files API scope on the Private App.

```bash
curl -sS -X POST "$BASE/internal/v1/tools/hubspot_get_attachment" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"account":"advertising","file_id":"FILE_ID"}}'
```

---

## 18. Approval-gated note write

Server env (required for writes):

```bash
HUBSPOT_WRITES_ENABLED=true
# HUBSPOT_WRITE_APPROVAL_TTL_SEC=600
```

Private App needs notes write + association scopes. No Slack involved — propose → token → confirm. **Writes require `account`** (no all-portals fan-out).

### Propose (does not create yet)

```bash
curl -sS -X POST "$BASE/internal/v1/tools/hubspot_propose_create_note" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"account":"advertising","body":"Follow-up from collab API","associate_object_type":"contacts","associate_id":"CONTACT_ID"}}'
```

Response includes `approval_token` + `expires_at` + `preview`.

### Confirm

```bash
curl -sS -X POST "$BASE/internal/v1/tools/hubspot_confirm_write" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"approval_token":"hs_write_..."}}'
```

### Cancel

```bash
curl -sS -X POST "$BASE/internal/v1/tools/hubspot_cancel_write" \
  -H 'Content-Type: application/json' \
  -H "X-Lucos-Org-Id: $ORG" \
  --data-binary '{"environment":"staging","args":{"approval_token":"hs_write_..."}}'
```

---

## Response shape (search)

```json
{
  "ok": true,
  "tool": "hubspot_search_companies",
  "account": "advertising",
  "portal_id": 244656364,
  "result_count": 10,
  "total": 44,
  "has_more": true,
  "next_after": "...",
  "filters_applied": {
    "last_activity_on": "2026-08-25",
    "timezone": "Asia/Kolkata",
    "date_property": "notes_last_updated",
    "sort": "createdate:descending"
  },
  "ui": {
    "timezone": "Asia/Kolkata",
    "sort": "createdate:descending",
    "object_type": "companies",
    "columns": ["name", "company_owner", "createdate_local", "phone", "last_activity_local", "city"]
  },
  "rows": [
    {
      "id": "...",
      "object_type": "companies",
      "name": "Y Combinator",
      "company_owner": "Garvita Sharma",
      "createdate_local": "2026-08-25, 22:12",
      "last_activity_local": "2026-08-25, 22:37",
      "phone": null,
      "city": null
    }
  ]
}
```

Every record also has canonical `id`, `object_type`, `created_at` / `updated_at` (+ `*_local`).

---

## Notes for ChatGPT / callers

- Omit `account` (or pass `account=all`) on search/list/get to query all four portals. Report per-portal counts from `by_account`.
- Pass `account` (`influential` | `advertising` | `adcom` | `monetize`, or labels like `Ad.com`) to scope to one portal. Single-account response shape is unchanged (`account` + `portal_id`, no `searched_all_accounts`).
- Pagination: `account=<slug>` + `after=<that portal’s by_account.next_after>`. Do not pass `after` on an all-accounts call.
- Writes still require `account`.
- Default timezone **Asia/Kolkata** to match HubSpot UI GMT+5:30.
- `created_on` / `last_activity_on`: `yesterday` | `today` | `YYYY-MM-DD`.
- `last_activity_week`: `current` → HubSpot **Last Activity Date** (`notes_last_updated`).
- Deal month filters use **`closedate`**, not create date.
- List pages return `next_after` + `has_more`.
- Writes are **propose → confirm** only; nothing hits HubSpot until confirm.
- Pass `"sort": "last_activity"` only if you want Last Activity ↓ instead of Create Date ↓.
