SOURCE_ID: import_from_hubspot NAME: Import HubSpot Contacts / Companies CATEGORY: CRM Pull HubSpot contacts / companies / deals into Floqer — either as a one-time import or as an ongoing source that re-pulls on a schedule. Imported records become available to your Floqer workflows for enrichment, scoring, outreach, and the like. INDEX: 1. Endpoints 2. Import modes 3. Pull mode (one-time vs ongoing) 4. Body shape (preview + create) 5. Filter operators by HubSpot property type 6. Dynamic options (mandatory pre-calls) 7. How to configure end-to-end 8. Key notes 9. Where it fits 10. When to use ================================================================================ 1. ENDPOINTS ================================================================================ Source identifier (used in every endpoint path): `import_from_hubspot`. POST /api/v1/sources/import_from_hubspot/preview Scope: sources:read Returns up to 100 records that WOULD be imported for the given body, without creating anything. Use this to validate filters / list selections before committing. POST /api/v1/sources/import_from_hubspot Scope: sources:write Creates the source, imports matching records immediately (when `pull_existing`), and — for an active source — keeps re-importing on a schedule. Returns `source_instance_id` (the new source's UUID). POST /api/v1/sources/import_from_hubspot/options/ Scope: sources:read Resolves dynamic option values for a field. For HubSpot, two `field_name` values matter: `hubspot_lists` and `hubspot_properties` (see §6). GET /api/v1/sources//data Scope: sources:read Paginated rows imported into the created source. `` is the UUID returned by Create. Query: `page_no` (default 1), `page_size` (default 20, max 200). Source-agnostic; see concepts.txt §10. POST /api/v1/sources//sync Scope: sources:write Connects the created source to a workflow and (by default) backfills it with the source's current rows. `` here is the UUID returned by Create. Body: { workflow_id, field_mapping, push_existing?, run? } — `field_mapping` keys are `input.` references on the target workflow, values are the source fields to pull. This step is source-agnostic; see concepts.txt §10 for the full shape. ================================================================================ 2. IMPORT MODES ================================================================================ `import_mode` is required on every preview/create call. Public values: "from_list" Pull records that are members of one or more HubSpot lists. The only selection criterion is list membership — no field-level filters are applied. Use this when the right audience already lives in a HubSpot list. Requires: `lists` (non-empty array of HubSpot list IDs as stringified numbers). Optional: `properties` (defaults to ALL properties when omitted — expensive; prefer passing an explicit list). "native_filters" Select records with a HubSpot CRM v3 Search filter expression (operator-based filter groups). The filter shape is passed through to HubSpot verbatim — operators come from HubSpot's catalogue. Requires: `properties` (non-empty), `filters` (with non-empty `filterGroups`). Optional: `lists` (restrict the search to those lists in addition to the filter expression). Any other value is rejected with a 400. ================================================================================ 3. PULL MODE (ONE-TIME VS ONGOING) ================================================================================ `pull_mode` is required on the create endpoint (not on preview). Public values: "static" One-time import. The source imports matching records once (when `pull_existing`) and stops — no recurring schedule. `max_count` caps the import size. "active" Ongoing source. Imports matching records initially (when `pull_existing`) AND re-imports on the cadence in `schedule` (a cron string), continuing until `expiration_date`. `max_count` is ignored for an active source (it stays in sync indefinitely). `schedule` is required when `pull_mode: active`. Otherwise it's ignored. ================================================================================ 4. BODY SHAPE (PREVIEW + CREATE) ================================================================================ Preview accepts (all snake_case): import_mode required: "from_list" | "native_filters" object_type required: "contacts" | "companies" | "deals" properties array; required for native_filters lists array; required for from_list filters {filterGroups: [{filters: [{propertyName, operator, value?}]}]}; required for native_filters properties_metadata array<{name, label, type?, ...}>; optional max_count integer | string; optional (preview always caps at 100) Create accepts all the preview fields plus: name required: display name for the new source pull_mode required: "static" | "active" schedule cron string; required when pull_mode === "active" expiration_date ISO date (YYYY-MM-DD); optional (active source only) pull_existing boolean; optional, defaults to true max_count cap on rows imported (static only). Optional. Omit: imports every matching record. Set to N: caps the import at N rows. N MUST NOT exceed the Preview's `metadata.total_results` — Recommended: set it equal to `metadata.total_results`, or omit to import all matches. Do not pick generic "safe" values like 100. Unknown top-level keys are rejected with 400 — the schema is strict. Each returned row also carries a `uid` field — the record's HubSpot object ID, surfaced as a plain string (not wrapped as `{label, value}`). Use it as the stable per-row identifier; it appears in both Preview and Get Source Data rows. Filter expression shape (only used in native_filters mode) — this is HubSpot's CRM v3 Search API shape, passed through verbatim: filters: { filterGroups: [ { filters: [ { propertyName: "", operator: "", value?: , values?: , highValue?: } ] } ] } Inner `filters[]` within ONE group are AND-combined. Multiple outer `filterGroups[]` are OR-combined. Limits (HubSpot CRM v3 Search API): at most 5 `filterGroups`, at most 6 `filters` per group, and at most 18 `filters` in total across all groups. Exceeding any of these is rejected up front with a 400. §5 below has the full operator catalogue keyed by HubSpot property type, plus the value-shape rules per operator. **Operators are type-dependent** — always look up the property's HubSpot `type` via dynamic options before picking an operator, or HubSpot will return a 400. ================================================================================ 5. FILTER OPERATORS BY HUBSPOT PROPERTY TYPE ================================================================================ Used by `import_mode: native_filters` only. Operators are TYPE-DEPENDENT — the set of valid operators for a property depends on that property's HubSpot `type`. Passing an operator unsupported for the property's type makes HubSpot return a 400 (surfaced as a 502 on preview). How to find a property's type: POST /api/v1/sources/import_from_hubspot/options/hubspot_properties Body: {"context": {"object_type": "contacts"}} Each returned option's `extras.type` is the property's HubSpot type (`number`, `date`, `datetime`, `string`, `enumeration`, etc.). Operator catalogue (matches the HubSpot Search API): ──────────────────────────────────────────────────────────────────── PROPERTY TYPE: number ──────────────────────────────────────────────────────────────────── EQ is value: NEQ is not value: LT less than value: LTE less than or equal to value: GT greater than value: GTE greater than or equal to value: HAS_PROPERTY is not empty (no value) NOT_HAS_PROPERTY is empty (no value) BETWEEN in between value: , highValue: ──────────────────────────────────────────────────────────────────── PROPERTY TYPE: date / datetime ──────────────────────────────────────────────────────────────────── EQ is value: NEQ is not value: LT is before value: GT is after value: HAS_PROPERTY is not empty (no value) NOT_HAS_PROPERTY is empty (no value) BETWEEN in between value: , highValue: ──────────────────────────────────────────────────────────────────── PROPERTY TYPE: string ──────────────────────────────────────────────────────────────────── EQ is value: "" NEQ is not value: "" IN in values: ["", "", ...] NOT_IN not in values: ["", "", ...] CONTAINS_TOKEN contains value: "" NOT_CONTAINS_TOKEN does not contain value: "" HAS_PROPERTY is not empty (no value) NOT_HAS_PROPERTY is empty (no value) ──────────────────────────────────────────────────────────────────── PROPERTY TYPE: enumeration (single-select / dropdown) ──────────────────────────────────────────────────────────────────── EQ is value: "" NEQ is not value: "" IN in values: ["", "", ...] NOT_IN not in values: ["", "", ...] HAS_PROPERTY is not empty (no value) NOT_HAS_PROPERTY is empty (no value) Valid enumeration values are listed in the property metadata's `extras.options[].value` — discoverable via `hubspot_properties`. Value-shape rules (apply universally): Single-value operators (EQ, NEQ, LT, LTE, GT, GTE, CONTAINS_TOKEN, NOT_CONTAINS_TOKEN): { propertyName, operator, value } Pass the value as a string for string/enumeration properties, as a number for number properties, and as either an ISO date string `"YYYY-MM-DD"` OR a Unix-ms timestamp for date / datetime. IN / NOT_IN: { propertyName, operator, values: ["v1", "v2", ...] } Use `values` (plural — an array), NOT `value`. Each item must match exactly. Common gotcha: passing IN with a single `value` string instead of `values` array → HubSpot returns 400. BETWEEN: { propertyName, operator: "BETWEEN", value: , highValue: } Inclusive on both ends. Lower bound goes in `value`, upper bound in `highValue` (NOT `values: [low, high]` — different field name). HAS_PROPERTY / NOT_HAS_PROPERTY: { propertyName, operator } Presence-only — pass NO `value`, `values`, or `highValue`. CONTAINS_TOKEN behavior: HubSpot tokenizes string values on whitespace and matches whole tokens, not substrings. `CONTAINS_TOKEN "hub"` does NOT match `"hubspot"` — it matches records whose tokens include the literal `hub`. Use full words with `CONTAINS_TOKEN`. Examples: Contacts created after 2026-01-01 (datetime field): { "propertyName": "createdate", "operator": "GT", "value": "2026-01-01" } Lifecycle stage is one of "customer" / "evangelist" (enumeration): { "propertyName": "lifecyclestage", "operator": "IN", "values": ["customer", "evangelist"] } Annual revenue in [1000000, 5000000] (number): { "propertyName": "annualrevenue", "operator": "BETWEEN", "value": 1000000, "highValue": 5000000 } Has an email address at all (any type): { "propertyName": "email", "operator": "HAS_PROPERTY" } Last name contains the token "smith" (string): { "propertyName": "lastname", "operator": "CONTAINS_TOKEN", "value": "smith" } AND-of-two + OR-of-groups: (createdate > 2026-01-01 AND lifecyclestage = customer) OR (closedate in 2026-Q1): { "filterGroups": [ { "filters": [ {"propertyName": "createdate", "operator": "GT", "value": "2026-01-01"}, {"propertyName": "lifecyclestage", "operator": "EQ", "value": "customer"} ]}, { "filters": [ {"propertyName": "closedate", "operator": "BETWEEN", "value": "2026-01-01", "highValue": "2026-03-31"} ]} ] } ================================================================================ 6. DYNAMIC OPTIONS (MANDATORY PRE-CALLS) ================================================================================ Two dynamic-options field names are available for this source. Both are POSTs to /api/v1/sources/import_from_hubspot/options/ with body `{ "context"?: {...} }`. hubspot_lists Context: none. Returns: {value: , label: , extras: {id, name, listType, ...}} Use the returned `value` directly inside `lists[]` on the create / preview body. hubspot_properties Context: { "object_type": "" } — REQUIRED. Returns: {value: , label: , extras: } Use the `value`s inside `properties[]`. To get labeled `{label, value}` rows in the preview / create response, pass the full returned array straight into `properties_metadata` on the body. Connection check: every endpoint for this source verifies the API-key user has an active HubSpot connection first. Missing connection → 424. ================================================================================ 7. HOW TO CONFIGURE END-TO-END ================================================================================ Typical flow for an AI agent building a HubSpot Contact-Added source: Step 1 — Resolve the list catalog (only needed for from_list) POST /api/v1/sources/import_from_hubspot/options/hubspot_lists Body: {} Pick the list IDs you want. Step 2 — Resolve the property catalog POST /api/v1/sources/import_from_hubspot/options/hubspot_properties Body: {"context": {"object_type": "contacts"}} Stash the full response array as `properties_metadata` (gets you {label, value} rows). Pick a subset of `.value`s as `properties`. Also note `.extras.type` for any property you plan to filter on — you'll need it in step 3 to pick the right operator (see §5). For enumeration properties, `.extras.options[].value` lists the valid right-hand-side values. Step 3 — Preview before committing POST /api/v1/sources/import_from_hubspot/preview Body (from_list): { "import_mode": "from_list", "object_type": "contacts", "lists": [""], "properties": ["email", "firstname", "lastname"], "properties_metadata": [...] } // from step 2 Body (native_filters): { "import_mode": "native_filters", "object_type": "contacts", "properties": ["email", "firstname", "lastname", "closedate"], "filters": { "filterGroups": [{ "filters": [ {"propertyName": "closedate", "operator": "GT", "value": "2026-01-01"} ]}] }, "properties_metadata": [...] } Check the returned `data[]` and `metadata.total_results`. Iterate the body until you're happy — preview never creates anything. Step 4 — Create the source POST /api/v1/sources/import_from_hubspot Body: + the create-only fields: "name": "", "pull_mode": "static" | "active", "schedule": "0 12 * * *", // only when pull_mode=active "expiration_date": "2027-01-01", // optional, active only "pull_existing": true, // optional, defaults true "max_count": // MUST NOT exceed Step 3's // metadata.total_results If Step 3 returned // total_results: 13, use max_count: 13 // (or omit to import all matches). // Don't pick 100 "to be safe". Response: { "status": 201, "data": { "source_instance_id": "", "name", "created_at" }} Step 4b — (Optional) Poll imported rows while the backfill runs GET /api/v1/sources//data?page_no=1&page_size=20 ( = UUID from Step 4) Row keys match Preview (property names you imported). `total_count` grows until import finishes. Step 5 — Sync the source into a workflow (so its records flow downstream) POST /api/v1/sources//sync ( = the UUID from Step 4 ) First build `field_mapping` (two lookups): a. GET /api/v1/workflows → pick the destination workflow_id. b. GET /api/v1/workflows//sheets//inputs → each input has a `reference` like `{{input.email}}`. c. Map each workflow input (reference WITHOUT braces) to a source field — the source fields are the property names you imported (Step 2 / the preview `data[]` keys). Body: { "workflow_id": "", "field_mapping": { "input.email": "email", "input.first_name": "firstname" }, "push_existing": true, // optional, default true "run": "all" } // optional, default "all" (none | first_10 | all) See concepts.txt §10 for the full sync semantics (source-agnostic). The initial import runs asynchronously — the create call returns as soon as the source is created and the import has been queued, not when records have finished arriving. Expect records to appear over seconds to minutes depending on payload size and HubSpot's pagination. ================================================================================ 8. KEY NOTES ================================================================================ - HubSpot connection is mandatory. If the API-key user has no active HubSpot connection, every endpoint (preview, create, options) returns 424 ("User is not connected to 'hubspot'."). Surface this as a "connect HubSpot first" CTA. - `from_list` falls back to ALL HubSpot properties when `properties` is empty (it fetches the property catalogue and uses every name). Always pass an explicit `properties` list to keep payloads thin and predictable. - `native_filters` evaluates your filter expression server-side; `from_list` selects purely by list membership. Both return the same row shape. Preview caps at 100 rows in either mode. - Total counts are best-effort. `metadata.total_results` comes from HubSpot's count endpoint, which occasionally lags writes by a few seconds — don't treat it as authoritative within the same minute as a HubSpot write. - Filter operators are validated by HubSpot, not by Floqer — they're passed through verbatim, so the catalogue in §5 stays in sync with HubSpot. An unsupported operator surfaces as a 502 on preview, or a failed import on create. - Credits are consumed when the source is created. If credit consumption fails (e.g. insufficient balance), the source is still created but its import can't start; the API returns 402 so you can top up and retry. - `properties_metadata` is optional. Each returned cell is a `{label, value}` pair; with metadata the `label` is the human label (`firstname: {label: "First Name", value: "Ada"}`), without it the label falls back to the property name (`firstname: {label: "firstname", value: "Ada"}`). Property VALUES are returned either way. - `max_count` is honored for a static source and ignored for an active one (an active source stays in sync indefinitely). On a static source `max_count` MUST NOT exceed the Preview's `metadata.total_results` Set it equal to the Preview total, or omit to import every match. ================================================================================ 9. WHERE IT FITS ================================================================================ UPSTREAM (in HubSpot) Contacts / companies / deals are created or updated in the user's HubSpot account. List memberships and property values come from HubSpot's own data model. THIS SOURCE Creates a HubSpot Contact-Added source. On creation it imports matching records immediately (when `pull_existing`); an active source also re-imports on the `schedule` cron until `expiration_date`. DOWNSTREAM (in Floqer) Imported records become available to your Floqer workflows — wire the source into a workflow to enrich, score, or run outreach on each new record automatically. ================================================================================ 10. WHEN TO USE ================================================================================ - One-time import of a curated HubSpot list for enrichment / outreach: `import_mode: from_list`, `pull_mode: static`. - Keep a saved HubSpot list (e.g. "Current Customers") in sync with a Floqer workflow on a schedule: `import_mode: from_list`, `pull_mode: active`, `schedule: "0 12 * * *"`. - "Pull every contact that matches X" where X is a filter expression (e.g. job title contains "VP" AND close date in the next 30 days): `import_mode: native_filters`, `pull_mode: static` (one-time snapshot) or `pull_mode: active` (stay in sync as new contacts match). When you instead want to push data OUT of Floqer INTO HubSpot, use the HubSpot action templates (e.g. `hubspot_create_object`, `hubspot_upsert_object`) inside a workflow — see https://floqer.com/docs/action-detail/hubspot_create_object.txt. ================================================================================ This file is maintained manually. Last updated: 2026-05-23. Full interactive reference: https://floqer.com/docs/reference