HubSpot API: get attachments from notes and emails
By Kayabyte
Attachments in HubSpot are not a property of the contact. They hang off engagements (notes, emails, calls, meetings, tasks) that are associated with the record. This guide explains the chain of calls, the scopes, and why Files API calls return 404 or 429. Endpoint paths and findings below are what we used and saw while building an exporter on 11-12 November 2026 (API date version 2026-09); check HubSpot’s reference for your version.
The chain
- Find the records. Read contacts, companies, deals or tickets with the CRM objects API (
/crm/objects/2026-09/{objectType}, search or batch read), or the members of a list (/crm/lists/2026-09/{listId}/memberships). - Find their engagements. Use the associations batch read (
POST /crm/associations/2026-09/{fromType}/{toType}/batch/read) from the record type to notes, emails, calls, meetings and tasks. Page through results when a record has many. - Read the attachment IDs. Batch-read the engagements with the properties hs_attachment_ids and hs_timestamp. hs_attachment_ids is a semicolon-separated string of file IDs, or empty. We saw the property on all five engagement types.
- Read the file.
GET /files/2026-09/files/{fileId}returns the name, extension, size and access level. HubSpot’s type field is a category such as DOCUMENT or IMG, not a MIME type, so derive the MIME type from the extension. The Files search call (GET /files/2026-09/files/search?ids=...) returns metadata for up to 100 files at once, and files that do not come back are not visible to your token. - Download it. Public files can be fetched from their URL. Private files return 404 there; request a signed URL with
GET /files/2026-09/files/{fileId}/signed-urland download from that.
Scopes
In our build the working set for read-only access was: files.read, files.ui_hidden.read, crm.objects.contacts.read (also enough for notes, calls, meetings and tasks in our tests), crm.objects.companies.read, crm.objects.deals.read, crm.objects.tickets.read, crm.lists.read and sales-email-read for emails. The older scope names files and tickets are now files.read and crm.objects.tickets.read: an upload with the old names failed. Files marked hidden (every attachment added through the UI is HIDDEN_PRIVATE) need files.ui_hidden.read; without it HubSpot answers 404 as if the file did not exist.
Why you get 404
- The file was deleted (HubSpot may mark it archived).
- The file is private and you used its normal URL instead of a signed URL.
- The file is hidden and your token lacks files.ui_hidden.read.
Treat a 404 as “missing” only after you have checked the scope.
Sensitive files
Files with a sensitive access level cannot be fetched with a signed URL (“Cannot access sensitive file”). HubSpot’s download endpoint for them needs a separate permission that staff describe as for privately distributed apps (community thread). Whether a Marketplace app can request it is something we did not confirm, so our app does not.
Rate limits
HubSpot’s documentation gives Marketplace OAuth apps 110 requests per 10 seconds per installing account (CRM search has its own limit). Read the X-HubSpot-RateLimit headers and wait for Retry-After on a 429. A large export is many calls: metadata for each file and one signed URL per private file.
Other traps we met
- HubSpot renames same-named uploads, so two files can have names that look unrelated.
- Names can contain characters that are illegal on Windows or macOS paths; clean them before writing to disk.
- The Files API import-from-URL refused to create hidden files in our test account, so test data for hidden attachments has to be made in the HubSpot UI.
When to use an app instead
If you need this once or twice, a script is fine. If you need a manifest, restart after a failure and many thousands of files, you are building an exporter. Full disclosure: we built Attachment Exporter: Bulk File Download, which does this chain, streams files into zips and writes the manifest. Free to 250 files, then packs from USD 49; installs from our site, not the HubSpot Marketplace.
FAQ
Where are attachments stored in the API? In hs_attachment_ids on engagements, not on the contact.
Which scope do I need for attachments on notes? files.read plus files.ui_hidden.read, and the record read scope for the object.
Why is a signed URL needed? Private and hidden files give 404 at their normal URL.
What is the rate limit? 110 requests per 10 seconds per account for Marketplace OAuth apps, per HubSpot’s documentation.
Questions or corrections: support@kayabyte.dev. HubSpot is a registered trademark of HubSpot, Inc. Kayabyte is an independent developer and is not affiliated with HubSpot or the other products named here.
More guides
- How to audit and clean up HubSpot properties without breaking anything
- How to stop a HubSpot deal reaching Closed won until it is approved
- How to bulk create custom properties in HubSpot from a CSV or spreadsheet
- How to calculate MRR from Paddle: yearly, weekly, discounts and tax
- How to send a client an hours statement from HubSpot
- How to copy properties from one HubSpot account to another
- DealHub or a lightweight approval app for HubSpot deals?
- How to download all attachments in HubSpot
- How to export all properties from HubSpot, with their dropdown options
- Export files from HubSpot: library vs attachments
- Export ticket and deal attachments from HubSpot
- Find sensitive files in HubSpot attachments
- HubSpot attachment export tools compared
- How to bulk add dropdown options (and properties) in HubSpot
- HubSpot deal approval options compared: native approvals, workflows, DealGuard and Deal Approvals & Stage Gate
- HubSpot deal approvals without Enterprise: what Free, Starter and Professional can do
- HubSpot discount approval on Sales Hub Professional or Starter
- How to set up discount approvals in HubSpot (deal desk)
- HubSpot GDPR request: export a contact’s files
- HubSpot migration: export attachments checklist
- How to see what your HubSpot pipeline looked like last week
- How to track prepaid hours and retainers in HubSpot
- Creating HubSpot properties in bulk: HubSpot import, the API, Lift and Bulk Property Import & Export compared
- HubSpot property naming conventions: a template for internal names, groups and labels
- Tracking retainer hours in HubSpot: StopWatch, PSOhub, a spreadsheet or Retainer Wallet
- A weekly stale-deal report in HubSpot, without Enterprise
- Time tracking on HubSpot tickets: what HubSpot has and the options
- HubSpot workflows for failed Paddle payments, cancellations and ending trials
- Insycle or Bulk Property Import & Export for HubSpot properties?
- Paddle and HubSpot without Zapier: webhooks, Make and other options
- How to require approval before a deal moves stage in HubSpot
- How to require fields before a HubSpot deal moves to a stage
- Retainer rollover rules: expire, capped or full, with examples
- RevOps Deal Desk & CPQ vs Deal Approvals & Stage Gate for HubSpot
- How to show MRR on HubSpot contacts and companies
- How to stop reps skipping deal stages or moving deals backward in HubSpot
- How to sync Paddle subscriptions to HubSpot
- Export property history in HubSpot: who changed it and when
- How to track a retainer balance per client in HubSpot