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,exportedandPATCH /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
- Create the HubSpot workflow: trigger Deal stage = Closed won; Send a webhook (POST) with deal id, associated company id and contact id.
- 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"}'
- 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 /dayseach time (never incremented). - A correction returns a day to
submitted; filtering onstatus=approved,exportedexcludes 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 returnscreated: false. - Company domain lands in
mail_domains. - Deal totals equal the sum of
GET /daysminutes / 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_refin list endpoints; repeat the idempotentPUTto 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.