Vendor SFTP feeds into HubSpot custom objects on AWS

A private-markets wealth platform

  • Financial services
  • Canada
  • Integration, delivered in 2026
Fields mapped across five files
235
Vendor report formats handled
5
Files re-driven at go live, no failures
137
Fund detail records created at go live
1,353
Processing time, largest report
2 to 3 minutes

Two data providers deliver five CSV reports to a wealth platform by SFTP, and the CRM needed them as contacts with their holdings, transactions, fund details and engagement history attached. A file landing in storage now starts a four step workflow that parses it, validates 235 mapped fields against their types and allowed values, and batch loads the result into HubSpot. A file that fails lands in its own folder and emails the people who can fix it.

01 The Challenge

Both providers deliver scheduled CSV reports over SFTP. That is the interface the engagement had to build on, so the shape of the work was settled before it started.

The files are wide and they are not tidy. The three largest reports carry 46, 58 and 90 columns. Dates arrive in three formats. Currency arrives with a symbol and thousands separators. A single cell can hold several email addresses separated by a semicolon, a comma or a slash. Enumerated fields gain a new option whenever the provider adds one, with no notice. Some rows on the advisor report have no email address at all, which makes them unidentifiable rather than invalid.

The harder constraint was regulatory. This is financial services, and the client’s compliance lead set the real specification in a review: how long does our data sit on your side, what is the encryption standard on the transfer, who is allowed to upload, and how does an auditor see that the environment is being maintained. A pipeline that only moved data correctly would still have failed that review.

The two providers’ data also had to stay separated end to end, at rest and in flight, with no step that merges or compares across them.

02 The Solution

Managed SFTP gives each provider its own upload-only account, with no ability to delete or modify anything already there. Each maps to its own inbound folder, so the two feeds never share a path.

A file landing raises an event that starts a workflow of four ordered steps. Extract parses the CSV, moves it into a processing folder so the same file cannot run twice, and writes the rows out as JSON. Validate does the real work, Load writes to HubSpot, and a fourth step moves the file to a processed folder.

Validation lives in configuration rather than in code. Each file type has a mapping of source field to HubSpot property, a type, and for enumerated fields the allowed values. The validator strips the byte order mark from the first header, matches header names case insensitively, remaps each field, then runs the validator for its type: emails split on any of the three separators, with the first going to the email property and the rest to an overflow field; currency strips the symbol and the commas; dates accept all three formats.

Two decisions there matter more than the rest. An enumerated value that is not in the allowed list becomes “Other”, and the original value is kept in a companion property, so a new option from a provider never stops a file. And a row that cannot be identified is skipped and counted rather than failed, while a row that is identifiable but wrong fails the batch, because a partly loaded file is worse than a rejected one.

Load runs in batches of 100. Contacts upsert on email with the advisor registration number as the fallback key. Four custom objects hold what a contact cannot: fund details, engagement activities, holdings and transactions, each associated back to the contact that owns it.

03 The Outcome

Go live was a sandbox to production cutover, ordered so that a mistake stayed recoverable. We took a full snapshot of the production account, then ran an automated schema comparison against it, which found five properties missing from one custom object. Those were created before any write.

Then the 137 files already processed in the sandbox were re-driven through the production pipeline in batches of 10, with record counts read back from the CRM after each batch. All 137 completed with no failures, producing 1,353 fund detail records, 178 engagement activities and 386 contacts. Contacts already in the portal that matched nothing incoming were untouched. A run takes about 30 seconds for the small activity report and 2 to 3 minutes for the larger detail report.

That go live covered the first provider’s two reports. The second provider’s three reports are built and tested, and wait on the provider before they run in production, so none of the counts above include them.

The compliance answers are now in the delivered documentation rather than folklore: a rolling one month retention, the named cipher suites, upload only accounts scoped to one folder each, and a monthly report covering patches, security status and risks found.

How the lifecycle runs now

  1. File arrives

    Each provider, upload only

    1. Source: Provider uploads a CSV report over SFTP Next: Lands in that provider's own inbound folder.
    2. Lands in that provider's own inbound folder Next: The landing raises an event.
    3. The landing raises an event Next: CSV parsed, in Extract.
  2. Extract

    Extract step, one retry

    1. CSV parsed Next: Moved to processing, rows written as JSON.
    2. Moved to processing, rows written as JSON Next: Headers matched, every field remapped and typed, in Validate.
  3. Validate

    Validate step, one retry

    1. Headers matched, every field remapped and typed Next: Unlisted value becomes "Other", original kept.
    2. Unlisted value becomes "Other", original kept Next: Can the row be identified?.
    3. Decision: Can the row be identified? Yes: Contacts upsert on email, with a fallback key, in Load. No: Skipped and counted, not failed.
    4. Skipped and counted, not failed
  4. Load

    Load step, batches of 100, two retries

    1. In HubSpot: Contacts upsert on email, with a fallback key Next: Four custom objects, each tied to its contact.
    2. In HubSpot: Four custom objects, each tied to its contact Next: Any step failed after its retries?, in Outcome.
  5. Outcome

    The state machine

    1. Decision: Any step failed after its retries? No: File moved to the processed folder. Yes: Failed folder, alert routed by provider.
    2. Outcome, done: File moved to the processed folder
    3. Outcome, stops: Failed folder, alert routed by provider
The map follows one vendor file from upload to the folder it ends in, through extract, validate and load. A file ends in processed, or in failed with an alert when any step fails after its retries.

04 Stack

The pipeline is AWS serverless: managed SFTP, object storage with a folder tree per provider, an event rule, a state machine and three functions in Node.js and TypeScript.

The state machine has 21 states and only four do the work. The other 17 are error handling and endings: three build an error context, three move the file to failed, three choose a destination by provider, six publish the alert, and two are terminal. Retries are per step: one each on extract and validate, two with backoff on the load because a rate limit is worth waiting out, none on the move.

Nothing needs patching at the operating system level, which answered most of the maintenance question. The debt that exists is written down rather than buried: the runtime and the cloud SDK are both past end of life, each with a written upgrade path. There is no infrastructure as code, because the stack is built once and left running.

05 Architecture and data model

Five report files arrive by SFTP into per-provider inbound folders. A file landing raises an event, the workflow extracts, validates and loads it, and the file moves to a processed folder. The only branch is the failure path: any step failing after its retries moves the file to a failed folder and sends an alert routed by provider, and nothing retries on its own after that. Recovery is re-uploading a corrected file, which starts a clean run.

Direction is one way for every object. HubSpot is never read back into a vendor file, and no step compares one provider’s rows against the other’s, in storage or in memory. A row that cannot be keyed at all is counted and skipped rather than failed, so a file’s row count and its load count rarely match, and that difference is the number worth watching.

The mapping runs to 235 rows across the five files. The excerpt below is 12 of them, chosen to show a contact field, a custom object field, a required key and an allowed value list.

Source fileHubSpot objectDirectionMatch key
User detail reportContactTo HubSpotEmail, or the advisor registration number
User detail reportFund details, custom objectTo HubSpotFund id joined to the advisor email
Engagement activity reportEngagement activities, custom objectTo HubSpotCreated each run, associated by email
Shareholder names and addressesContactTo HubSpotEmail, or the shareholder id
ShareholdingsHoldings, custom objectTo HubSpotShareholder id
Shareholder transactionsTransactions, custom objectTo HubSpotDeal number
Source fileSource fieldTypeHubSpot propertyRule
User detail reportFund NamestringFund NameRequired
User detail reportPAF IDstringPAF / Fund IDRequired
User detail reportAdvisor E-mailemailE-mailSplit on semicolon, comma or slash
User detail reportAdvisor CRDstringAdvisor CRDFallback key when there is no email
User detail reportRole TypeenumerationRole TypeSales, AI/QP, QP, Delegate, RIA, SFO, Interested Parties, Interested Party
User detail reportUser AccreditationbooleanUser AccreditationAccepts true, false, 1, 0, yes, no, t, f, y, n
Engagement activity reportActivityenumerationActivityCourse Passed, Document Viewed, Fund Followed, Fund Unfollowed, Product Summary, Viewed Fund Profile
Engagement activity reportEngagement ScorenumberEngagement ScoreNumeric check only
Shareholdingsshareholder_idstringShareholder IDRequired, and the association key
Shareholdingsvaluation_datedateValuation DateISO 8601, or two written date formats
Shareholdingspercent_holdingnumberPercent HoldingNumeric check only
Shareholder transactionsTRADE_TYPEenumerationTrade TypeRedeem, Redeem Full, Issue, Transfer, Transfer Full
Two vendors upload five CSV reports over SFTP into their own storage folders. A file landing raises an event that starts a four step workflow: extract the rows, validate and remap every field, batch load contacts and four custom objects into HubSpot, then move the file to a processed folder. Any step failing moves the file to a failed folder and sends an email alert.
Hosting
AWS serverless, one region
Runtime
Node.js and TypeScript functions, sequenced by a state machine
Cadence
Event driven, a run starts when a file lands
Pattern
SFTP file pipeline
Fields mapped
235

235 mapped fields across five vendor files, keyed on the contact's email with the vendor's own identifier as the fallback.

What we would do again, and differently

What we would do again

  • Keep the field mapping and the allowed values as configuration rather than code, so a new column from a provider is an edit and not a deployment.
  • Let an unexpected enumeration value fall through to “Other” with the original value preserved beside it, because a file blocked at two in the morning costs more than an odd value a human can read later.

What we would do differently

  • Agree the retention policy in the first week, when it is one lifecycle rule, rather than at the compliance review when it is a change request against a system already running.
  • Pin the runtime and the cloud SDK to supported releases at build time, instead of handing over an upgrade as the first maintenance item.

Systems in this build

Partnerships and accreditations

  • HubSpot Elite Solutions Partner
  • HubSpot Custom Integration Accreditation
  • HubSpot CRM Data Migration Accreditation
  • HubSpot Solutions Architecture Design Accreditation

HubSpot accreditations

  • HubSpot Onboarding Accreditation
  • HubSpot Academy Accreditations

Industry specializations

  • HubSpot Healthcare Industry Specialist
  • HubSpot Financial Services Industry Specialist
  • HubSpot Manufacturing Industry Specialist
  • HubSpot Quote to Cash Capability

Partnerships

  • Google Partner
  • Anthropic Solutions Partner

Have a system in the same shape?

Send the systems, the direction and the record volume. We reply with a scope and an effort band within one business day.

Press Enter or comma to add another system.

Add more detail (optional)

Optional. A sentence or two about the workflow is enough.

We use these answers to scope the work and for nothing else. Privacy policy.

Prefer to talk it through first? Book a technical scoping call

Talk to the people who build these

  • Nijat
  • Sean
  • Prash

Book a scoping call