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

  1. 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).
  2. 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.
  3. 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.
  4. 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.
  5. 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-url and 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

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

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.