{
  "schema": "aaes/capability-pack/1",
  "provider": "hubspot",
  "version": "1.0.0",
  "description": "Read HubSpot contacts, companies, deals, and tickets; create contacts with optional associations to existing records. Covers one account and does not register email sending.",
  "notes": "Every endpoint here is on api.hubapi.com, the shared HubSpot API host named in HubSpot's own private-app documentation, so there is no per-customer host to substitute; HubSpot responses also reference the api.hubspot.com alias, which this pack deliberately does not allowlist — one authority per capability, exactly as registered. This pack registers only endpoints the official HubSpot developer documentation describes: the CRM v3 objects collection reads for contacts, companies, deals and tickets, and single-record contact creation.\n\nAll read capabilities cover the collection read only (GET /crm/v3/objects/{object}): single-record reads (GET /crm/v3/objects/{object}/{recordId}), batch reads, search and association navigation endpoints are not registered by this pack.\n\nDocumented exclusions: this pack does not register record update or delete (PATCH/DELETE /crm/v3/objects/{object}/{recordId}), batch create/update/archive operations, the standalone association-write endpoints (contact creation can itself establish associations to existing records through the associations object in the same POST — that is part of the registered create capability, and what is excluded is the standalone association-write surface), company, deal or ticket creation, engagements (notes, calls, meetings, tasks), and any email-sending surface (marketing email send, transactional single-send): a send capability would carry the send verb at the minimum R3 tier, and this pack registers none.\n\nData-class rationale: contacts and tickets identify individuals and carry their messages, so both are classified as pii (personal data); deals carry amounts, stages and close dates — the account's revenue pipeline — and are registered financial for the same reason the Salesforce pack classes opportunity data financial; companies describe organizations rather than individuals and are registered internal, and a deployment whose company records carry personal or regulated data should raise the class before enabling.\n\nTiering: reads sit at R0 (the read floor) and contact creation at R2 (the create floor); the shipped pack registers at the minimum and policy can raise a tier for a specific actor, scope or resource.\n\nThe endpoints are verified against the provider documents named in the review block, and the custody review that docs/gtm/CAPABILITY-LIBRARY-PLAN.md §4 gates authentication modes on was completed on 2026-09-21 against this pack's custody model (a private app access token held in the customer's vault), with these findings, one per gated property.\n\nIssuance authority and delegation: a super admin creates the private app in the HubSpot account (Development > Legacy apps > Create legacy app > Private), chooses its scopes and copies the token from the app's Auth tab, so issuance authority sits with the account's own super admin.\n\nScope: scopes are chosen at app creation and are per-object and per-operation for most CRM objects, with tickets the documented exception (the ticket-read scope is 'tickets', and no crm.objects.tickets.read exists); the least-privilege grant is crm.objects.contacts.read, crm.objects.companies.read, crm.objects.deals.read and tickets for the reads, plus crm.objects.contacts.write for the create capability — and nothing more.\n\nAudience: no audience mechanism is documented — the bearer value authorizes against api.hubapi.com, and audience binding beyond that is not established. TTL: private app access tokens do not expire on their own; HubSpot recommends rotating every six months and emails super admins when a token has gone unrotated for 180 days.\n\nRefresh ownership: there is no refresh — rotation is a manual action from the app's Auth tab ('Rotate and expire now' invalidates the original token immediately, 'Rotate and expire later' leaves it valid for 7 days), renewal ownership sits with an external deployment process, and the executor injects the stored value verbatim and performs no token lifecycle itself.\n\nRevocation: deleting the private app permanently revokes its token, and because the token is the credential (no separate tokens are issued from it), rotation or deletion revokes exactly this value. Executor access and replay/exposure: the executor reads only the vault reference; the token is a bearer credential replayable with no TTL bound until rotated or deleted, and HubSpot's 180-day unrotated-token reminder is the vendor's mitigation.\n\nAlternate access paths: the HubSpot UI and other apps or OAuth integrations retain access outside this pack, and deleting this app or rotating its token closes only this path. The credential review is complete. Assign accountable owners before deployment.",
  "credential": {
    "secret_ref_id": "secret-ref-hubspot-private-app-token",
    "custody": "customer_vault",
    "inject": "header",
    "inject_name": "Authorization",
    "note": "A HubSpot private app access token, held in the client's own secret manager. The stored material must be the complete Authorization header value: the scheme 'Bearer', one separating space, then the token — 'Bearer <token>'.\n\nToken issuance is an external process: a super admin creates the private app in the HubSpot account (Development > Legacy apps > Create legacy app > Private), chooses its scopes, and copies the token from the app's Auth tab; the executor performs no token lifecycle itself and injects the stored value verbatim.\n\nLeast privilege: HubSpot scopes are per-object and per-operation for most CRM objects (for example crm.objects.contacts.read versus.write), but tickets are the documented exception: HubSpot's scope list documents the ticket-read scope as 'tickets' (which includes access to tickets) — tickets do not follow the crm.objects.<object>.read pattern, and no crm.objects.tickets.read scope exists — so grant crm.objects.contacts.read, crm.objects.companies.read, crm.objects.deals.read and tickets for the reads, plus crm.objects.contacts.write for the create capability — and nothing more.\n\nExpiry and revocation: private app access tokens do not expire on their own; rotation is manual from the app's Auth tab — 'Rotate and expire now' invalidates the original token immediately, while 'Rotate and expire later' leaves it valid for 7 days — and deleting the private app permanently revokes its token.\n\nBecause the token is the credential (no separate tokens are issued from it), rotation or deletion revokes exactly this value; HubSpot recommends rotating every six months and emails super admins when a token has gone unrotated for 180 days.\n\nRenewal ownership sits with an external deployment process.\n\nExecutor hygiene: the value must live only in executor memory for the lifetime of a call, must never be written to logs, traces or diagnostics, and must not appear in crash dumps; a deployment that cannot meet that bar must not enable the pack."
  },
  "capabilities": [
    {
      "capability_id": "hubspot.contacts.read",
      "version": "1.0.0",
      "kind": "tool",
      "risk_tier": "R0",
      "owner": "REPLACE-WITH-ACCOUNTABLE-HUMAN",
      "data_class": "pii",
      "egress_allowlist": [
        "api.hubapi.com"
      ],
      "endpoint": "https://api.hubapi.com/crm/v3/objects/contacts",
      "method": "GET",
      "connector": "http",
      "custody": "customer_vault",
      "secret_ref_id": "secret-ref-hubspot-private-app-token",
      "inject": "header",
      "inject_name": "Authorization",
      "actions": [
        "read"
      ],
      "resource_id": "hubspot-account-contacts",
      "description": "List contact records: names, email addresses, phone numbers and other properties, which are personal data. The registered path covers the collection read only — single-contact reads (GET /crm/v3/objects/contacts/{recordId}), batch reads and search are not registered by this pack.",
      "requires_review": true
    },
    {
      "capability_id": "hubspot.companies.read",
      "version": "1.0.0",
      "kind": "tool",
      "risk_tier": "R0",
      "owner": "REPLACE-WITH-ACCOUNTABLE-HUMAN",
      "data_class": "internal",
      "egress_allowlist": [
        "api.hubapi.com"
      ],
      "endpoint": "https://api.hubapi.com/crm/v3/objects/companies",
      "method": "GET",
      "connector": "http",
      "custody": "customer_vault",
      "secret_ref_id": "secret-ref-hubspot-private-app-token",
      "inject": "header",
      "inject_name": "Authorization",
      "actions": [
        "read"
      ],
      "resource_id": "hubspot-account-companies",
      "description": "List company records: names, domains, industries and locations, which describe organizations and are registered internal. The registered path covers the collection read only — single-company reads (GET /crm/v3/objects/companies/{companyId}) and batch reads are not registered by this pack.",
      "requires_review": true
    },
    {
      "capability_id": "hubspot.deals.read",
      "version": "1.0.0",
      "kind": "tool",
      "risk_tier": "R0",
      "owner": "REPLACE-WITH-ACCOUNTABLE-HUMAN",
      "data_class": "financial",
      "egress_allowlist": [
        "api.hubapi.com"
      ],
      "endpoint": "https://api.hubapi.com/crm/v3/objects/deals",
      "method": "GET",
      "connector": "http",
      "custody": "customer_vault",
      "secret_ref_id": "secret-ref-hubspot-private-app-token",
      "inject": "header",
      "inject_name": "Authorization",
      "actions": [
        "read"
      ],
      "resource_id": "hubspot-account-deals",
      "description": "List deal records: amounts, pipeline stages and close dates — the account's revenue pipeline, registered financial for the same reason opportunity data is financial in the Salesforce pack. An R0 financial read is still a serious confidentiality risk; the floor is a lower bound, not a business-risk assessment. The registered path covers the collection read only — single-deal reads (GET /crm/v3/objects/deals/{dealId}) and batch reads are not registered by this pack.",
      "requires_review": true
    },
    {
      "capability_id": "hubspot.tickets.read",
      "version": "1.0.0",
      "kind": "tool",
      "risk_tier": "R0",
      "owner": "REPLACE-WITH-ACCOUNTABLE-HUMAN",
      "data_class": "pii",
      "egress_allowlist": [
        "api.hubapi.com"
      ],
      "endpoint": "https://api.hubapi.com/crm/v3/objects/tickets",
      "method": "GET",
      "connector": "http",
      "custody": "customer_vault",
      "secret_ref_id": "secret-ref-hubspot-private-app-token",
      "inject": "header",
      "inject_name": "Authorization",
      "actions": [
        "read"
      ],
      "resource_id": "hubspot-account-tickets",
      "description": "List ticket records: customer requests for help, which name individuals and can quote their messages, so they are classified as pii (personal data). The registered path covers the collection read only — single-ticket reads (GET /crm/v3/objects/tickets/{ticketId}) and batch reads are not registered by this pack.",
      "requires_review": true
    },
    {
      "capability_id": "hubspot.contacts.create",
      "version": "1.0.0",
      "kind": "tool",
      "risk_tier": "R2",
      "owner": "REPLACE-WITH-ACCOUNTABLE-HUMAN",
      "data_class": "pii",
      "egress_allowlist": [
        "api.hubapi.com"
      ],
      "endpoint": "https://api.hubapi.com/crm/v3/objects/contacts",
      "method": "POST",
      "connector": "http",
      "custody": "customer_vault",
      "secret_ref_id": "secret-ref-hubspot-private-app-token",
      "inject": "header",
      "inject_name": "Authorization",
      "actions": [
        "create"
      ],
      "resource_id": "hubspot-account-contacts",
      "description": "Create one contact record with a properties object (HubSpot requires at least one of email, firstname, lastname) and optional associations to existing records. Creating a CRM record sends nothing to the person it describes, so the declared verb is create at the minimum R2 tier; batch create and upsert are not registered by this pack.",
      "requires_review": true
    }
  ],
  "resources": [
    {
      "resource_id": "hubspot-account-contacts",
      "kind": "api",
      "locator": "https://api.hubapi.com/crm/v3/objects/contacts",
      "data_class": "pii",
      "owner": "REPLACE-WITH-ACCOUNTABLE-HUMAN"
    },
    {
      "resource_id": "hubspot-account-companies",
      "kind": "api",
      "locator": "https://api.hubapi.com/crm/v3/objects/companies",
      "data_class": "internal",
      "owner": "REPLACE-WITH-ACCOUNTABLE-HUMAN"
    },
    {
      "resource_id": "hubspot-account-deals",
      "kind": "api",
      "locator": "https://api.hubapi.com/crm/v3/objects/deals",
      "data_class": "financial",
      "owner": "REPLACE-WITH-ACCOUNTABLE-HUMAN"
    },
    {
      "resource_id": "hubspot-account-tickets",
      "kind": "api",
      "locator": "https://api.hubapi.com/crm/v3/objects/tickets",
      "data_class": "pii",
      "owner": "REPLACE-WITH-ACCOUNTABLE-HUMAN"
    }
  ],
  "review": {
    "state": "library",
    "date": "2026-09-22",
    "sources": [
      "https://developers.hubspot.com/docs/api/crm/contacts (retrieved 2026-09-21)",
      "https://developers.hubspot.com/docs/api/crm/companies (retrieved 2026-09-21)",
      "https://developers.hubspot.com/docs/api/crm/deals (retrieved 2026-09-21)",
      "https://developers.hubspot.com/docs/api/crm/tickets (retrieved 2026-09-21)",
      "https://developers.hubspot.com/docs/api/private-apps (retrieved 2026-09-21)",
      "https://developers.hubspot.com/docs/api/scopes (retrieved 2026-09-21)"
    ],
    "reviewer_id": "aaes-eng"
  }
}
