Skip to content

Request an integration

Tell us which system you want to connect. We review every request weekly and publish a step-by-step guide or build the connection.

HubSpot integration

Create TimeTracking clients and placements from HubSpot companies and won deals with external-ID upserts, and write approved-hour totals back using GET /days.

Last updated: · Markdown version

1. Summary

Status: Recipe (no native connector).

Direction: HubSpot to TimeTracking for clients and placements; TimeTracking to HubSpot for approved-hour totals (optional).

Typical use: an agency sells with HubSpot. When a deal is won, the client and placement must exist in TimeTracking. Approved hours can be written back to the deal for account management.

2. Mechanism

  • HubSpot side. A workflow ("Deal stage is Closed won") with Send a webhook (Operations Hub Professional), or an iPaaS trigger (Zapier, Make, n8n). Objects via CRM v3 (/crm/v3/objects/companies, /deals, /contacts, associations).
  • TimeTracking side. PUT /clients/by-external/hubspot/{companyId}, PUT /people/by-external/hubspot/{contactId}, PUT /placements/by-external/hubspot/{dealId}. HubSpot ids are the keys, so nothing has to be written back to HubSpot to keep the link.
  • Back-sync. GET /days?from&to&placement_id=…&status=approved,exported and PATCH /crm/v3/objects/deals/{id}.

Assumption — verify with your HubSpot admin: many agencies model placements as a custom object ("Placement") or as deals with custom properties. Adapt the mapping.

3. Prerequisites

  • HubSpot private app token with crm.objects.companies.read, crm.objects.deals.read/write, crm.objects.contacts.read (plus custom object scopes if used).
  • TimeTracking API key (write). A read-only key is enough for the back-sync (GET /days, GET /placements/{id}).
  • Custom HubSpot property for totals: tt_approved_hours_this_month (number).

4. Entity mapping

HubSpot object.property TimeTracking field Notes
Company hs_object_id client external ref
Company name name
Company domain mailDomains Matches inbound timesheet emails from that domain.
Contact (worker) hs_object_id person external ref Only if workers live in HubSpot; often they come from an ATS.
Contact firstname, lastname, email, phone, employee_number person fields
Deal / custom "Placement" hs_object_id placement external ref; ref HS-<id>
Deal start_date / end_date (custom) startsOn / endsOn
Deal job_title / dealname roleTitle
Sum of GET /days minutes for the deal's placement tt_approved_hours_this_month Hours = minutes / 60.

External-ID strategy. source = hubspot. Upserts are idempotent and full replaces. To find the TimeTracking placement for a deal, call PUT again (idempotent, returns the record) or page GET /placements and match external_ref.

5. Step-by-step setup

  1. Create the HubSpot workflow: trigger Deal stage = Closed won; Send a webhook (POST) with deal id, associated company id and contact id.
  2. In the receiver/scenario:
curl -s -X PUT "$TT/clients/by-external/hubspot/1873402" -H "Authorization: Bearer $KEY" -H "content-type: application/json" \
  -d '{"name":"VDL Montage","externalRef":"1873402","approvalMode":"both","mailDomains":"vdl-montage.example"}'
curl -s -X PUT "$TT/people/by-external/hubspot/551" -H "Authorization: Bearer $KEY" -H "content-type: application/json" \
  -d '{"employeeNumber":"1001","firstName":"Jan","lastName":"de Vries","email":"jan@example.com"}'
curl -s -X PUT "$TT/placements/by-external/hubspot/9921833" -H "Authorization: Bearer $KEY" -H "content-type: application/json" \
  -d '{"personExternalRef":"551","clientExternalRef":"1873402","ref":"HS-9921833","roleTitle":"Monteur","startsOn":"2026-11-02"}'
  1. Optional back-sync (scheduled, or on timesheet.approved):
curl -s "$TT/days?from=2026-12-01&to=2026-12-31&placement_id=<placement-uuid>&status=approved,exported&limit=500" \
  -H "Authorization: Bearer $KEY"
# sum items[].minutes / 60, then:
curl -s -X PATCH "https://api.hubapi.com/crm/v3/objects/deals/9921833" \
  -H "Authorization: Bearer $HUBSPOT_TOKEN" -H "content-type: application/json" \
  -d '{"properties":{"tt_approved_hours_this_month":"15.5"}}'

6. Sync schedule and triggering events

Trigger Action
HubSpot workflow: deal Closed won Upsert client, (person), placement.
HubSpot workflow: end_date changed Upsert the placement again (full replace).
Deal closed lost after start DELETE /placements/{id} (deactivates).
timesheet.approved webhook or nightly Recompute totals from GET /days.
conflict.created (optional) Create a HubSpot task for the account manager.

7. Corrections, conflicts and locked periods

  • HubSpot never edits hours. Totals are recomputed from GET /days each time (never incremented).
  • A correction returns a day to submitted; filtering on status=approved,exported excludes it until re-approved.
  • Exported/locked days (423) are final; corrections appear in later periods.

8. Testing checklist

  • Closing a test deal returns created: true; re-triggering returns created: false.
  • Company domain lands in mail_domains.
  • Deal totals equal the sum of GET /days minutes / 60.

9. Troubleshooting

Error Fix
422 "Person and client must exist …" Upsert company and contact first (same source hubspot).
422 on placements Dates not YYYY-MM-DD.
403 "Admin role required" Webhooks need an admin user or an admin-scope key.
429 Wait Retry-After (60 s).
HubSpot 401/403 Missing private app scope.

10. Limits and known gaps

  • No client-contact API; approvers are set in the dashboard.
  • No filter on external_ref in list endpoints; repeat the idempotent PUT to resolve a record.
  • HubSpot marketplace app with OAuth2: Planned.

11. Build it with Claude

Build a HubSpot ↔ TimeTracking integration as an n8n workflow (export JSON) or a TypeScript service.
Inbound: HubSpot workflow webhook on deal Closed won (dealId, companyId, contactId) → read via HubSpot CRM v3 →
TimeTracking PUT /clients/by-external/hubspot/{companyId} {name, mailDomains},
PUT /people/by-external/hubspot/{contactId} {firstName, lastName, email, employeeNumber},
PUT /placements/by-external/hubspot/{dealId} {personExternalRef, clientExternalRef, ref "HS-<dealId>",
roleTitle, startsOn, endsOn}. Upserts are full replaces and idempotent.
Outbound (nightly): GET /days?from&to&placement_id&status=approved,exported (follow next_cursor), sum minutes,
PATCH deal property tt_approved_hours_this_month. Base https://app.timetracking.tech/api/v1, Bearer key.
Ask me for my HubSpot property names first. No deletes.