CMS and websites
Upload images to HubSpot with the Files API
Send the images SocialCutter produces to the HubSpot File Manager with the Files API, by multipart or from a URL, and use them in emails, pages and social.
- HubSpot
- Files API
- File Manager
- import-from-url
- scopes
- SocialCutter
- email marketing
Why go through the File Manager
HubSpot’s File Manager is the portal’s media library: everything you insert into emails, pages, landing pages and blog posts is served from there, through HubSpot’s CDN. SocialCutter generates centered-crop versions at the exact dimensions of each destination and returns a public URL per output; if those images will live inside HubSpot, it is better to upload them to the File Manager than to link external URLs: you manage them, reuse them and index them with the rest of your assets.
The boundary matters: SocialCutter generates images, it does not publish. HubSpot will not publish to Instagram or LinkedIn for you from the File Manager either; it only stores and serves the file.
Requirements: Private app and scopes
Create a Private app under Settings → Integrations → Private apps and tick the Files API scopes:
| Scope | What it is for |
|---|---|
files | Read, upload and archive files and folders |
files.ui_hidden.read | Access hidden files that do not show in the public listing |
The token starts with pat- and travels in the Authorization: Bearer pat-… header. The complete Files API reference, with the current scopes and fields, lives at https://developers.hubspot.com/docs/api-reference/latest/files/guide.
Upload a file by multipart
POST /files/v3/files accepts multipart/form-data. One file per request:
curl -s -X POST "https://api.hubapi.com/files/v3/files" \
-H "Authorization: Bearer pat-your_token" \
-F "file=@instagram-post.jpg" \
-F "folderPath=/socialcutter" \
-F 'options={"access":"PUBLIC_INDEXABLE"}'
| Field | Required | Description |
|---|---|---|
file | Yes | The binary to upload |
folderId or folderPath | One of the two | Destination folder; do not upload to the root |
fileName | No | Final name; generated from the content if omitted |
options | No | JSON with access and, optionally, ttl (1 day to 1 year) |
The 201 response carries id, path, url, defaultHostingUrl, access, width, height and isUsableInContent. Keep the id and the url: they are what you use later in the email editor or the image picker.
Upload from a URL with import-from-url
When SocialCutter already returns URLs, you do not need to download and re-upload by hand. HubSpot imports from a URL asynchronously:
curl -s -X POST "https://api.hubapi.com/files/v3/files/import-from-url/async" \
-H "Authorization: Bearer pat-your_token" \
-H "Content-Type: application/json" \
-d '{
"url": "https://cdn.socialcutter.theboomer.dev/out/twitter-post.jpg",
"access": "PUBLIC_INDEXABLE",
"folderPath": "/socialcutter",
"duplicateValidationStrategy": "REJECT",
"duplicateValidationScope": "EXACT_FOLDER"
}'
The 202 response returns a task id. You poll the status with GET /files/v3/files/import-from-url/async/tasks/{taskId}/status, which answers PENDING, PROCESSING, COMPLETE or CANCELED. With COMPLETE the file is already in the File Manager.
Version note: HubSpot has been moving the Files API to versioned routes (from files/v3 to schemes such as files/2026-09), and scope names have changed between versions. Before hard-coding routes in your code, confirm the current route and scopes in the official documentation linked above.
Access levels and where each file can be used
access | Can it be inserted into emails, pages and landing pages? |
|---|---|
PUBLIC_INDEXABLE | Yes, and it can also be indexed by search engines |
PUBLIC_NOT_INDEXABLE | Yes, but not indexed |
PRIVATE | Not directly: it needs a signed URL and isUsableInContent is false |
SENSITIVE | No: meant for data, not for content |
If the image is going into an email or a landing page, use public access. A private file will not render in the email because the mail client cannot sign the URL.
HubSpot recommended sizes
These are the sizes HubSpot publishes for the places images are used inside the portal. For the exact dimensions that SocialCutter generates per social network, the reference is the internal measures guide (linked below).
| HubSpot placement | Recommended size | Ratio |
|---|---|---|
| Image inside an email | 600 px wide | Variable (template width rules) |
| Email header | 600x200 | 3:1 |
| Blog featured image | 1200x628 | ~1.91:1 |
| Social share image | 1200x630 | 1.91:1 |
| Blog thumbnail | 400x400 | 1:1 |
| Landing hero / full-width | 1920x1080 (≥1200 px wide) | 16:9 |
| Site banner | 2500x625 | 4:1 |
| Author image | 500x500 | 1:1 |
HubSpot sources: its social media sizes guide (https://blog.hubspot.com/marketing/ultimate-guide-social-media-image-dimensions-infographic) and its website image sizes guide (https://blog.hubspot.com/website/image-size-for-website). Watch the fine differences: HubSpot’s social share is 1200x630 and its blog featured image 1200x628; the closest SocialCutter destination is facebook + post (1200x630). If you need a size the catalogue does not produce, crop it separately.
Flow: from SocialCutter to the File Manager
- Process the master:
POST /api/v1/images/processwithsourceanddestinations. - Walk the
outputsarray and fire animport-from-url/asyncper URL. - Wait for
COMPLETEper task and collect the finalurl. - Insert that URL into the email, landing page or blog post.
import time, requests
HUB = {"Authorization": "Bearer pat-your_token"}
BASE = "https://api.hubapi.com/files/v3/files/import-from-url/async"
def upload(url, folder="/socialcutter"):
r = requests.post(BASE, headers={**HUB, "Content-Type": "application/json"},
json={"url": url, "access": "PUBLIC_INDEXABLE", "folderPath": folder},
timeout=30)
r.raise_for_status()
task = r.json()["id"]
while True:
s = requests.get(f"{BASE}/tasks/{task}/status", headers=HUB, timeout=30).json()
if s.get("status") in ("COMPLETE", "CANCELED"):
return s
time.sleep(2)
for out in outputs: # outputs comes from SocialCutter
print(out["platform"], upload(out["url"]))
Cost
| Concept | Value |
|---|---|
| Cost per processing | 1 use per destination (platform and format) |
| Duplicate destinations in the same request | Not charged twice |
| HubSpot File Manager upload | No extra cost, within the portal limits |
| SocialCutter plans | 0, 3, 9 and 29 EUR, with API and MCP included |
A request for Instagram post + LinkedIn post + X post uses 3 SocialCutter uses and produces 3 HubSpot uploads.
Common errors
| Error | What is really happening | What to do |
|---|---|---|
401/403 on upload | The token lacks the files scope | Add the scope in the Private app and regenerate the token |
| Image does not show in the email | It was uploaded as PRIVATE | Upload with public access (PUBLIC_INDEXABLE or PUBLIC_NOT_INDEXABLE) |
400 on the multipart upload | Missing folderId/folderPath or the folder does not exist | Create the folder first, or use import-from-url, which can create it |
429 | Portal rate limit | Add retries with exponential backoff |
| Duplicate name | An identical file already exists in the folder | Use duplicateValidationStrategy: RETURN_EXISTING or overwrite |
| The source URL will not import | HubSpot could not download the image | Check the URL is public and reachable without a session |
Next steps
- API guide with curl: Process images with the API from the terminal
- Python: Process images with the SocialCutter API from Python
- No-code: Automate image resizing with Zapier, Make or n8n
- Strategy: Automating social media images: the 4 real paths
- Sizes: Social media sizes: dimensions and ratios
- API documentation: https://docs.socialcutter.theboomer.dev
Frequently asked questions
Which scopes does the HubSpot token need?
The files scope to upload, plus files.ui_hidden.read if you also read hidden files. You grant them in a portal Private app, and the token starts with pat-.
Can I upload an image straight from its URL?
Yes. POST /files/v3/files/import-from-url/async downloads the image in the background and returns a task; you poll its status until it says COMPLETE. It is the natural path when SocialCutter already gives you a public URL.
Why does the image not show in a HubSpot email?
Almost always because the file was uploaded with access PRIVATE. A private file is only served with a signed URL and is not valid for emails or pages. Use public access for content.
How is processing billed?
1 use per destination, meaning each platform and format pair. HubSpot does not charge for uploads to the File Manager within the portal limits.
Who decides the crop, HubSpot or SocialCutter?
SocialCutter crops with a centered, deterministic crop before uploading: there is no subject or face detection. HubSpot only stores and serves the file you hand it.