Use CurlyFlies with n8n

One HTTP Request node.
A URL Instagram accepts.

Your generation step returns a signed URL that dies in an hour. Drop one node between it and your publish step, and every downstream API gets a public URL that serves raw bytes — for as long as your schedule needs.

The recipe.

One HTTP Request node, configured once. No community node install, no credentials wizard beyond a header.

STEP 01

Add an HTTP Request node after your generation step

Place it directly after the node that produces the image — OpenAI (GPT Image), Replicate, Higgsfield, an HTTP call to Seedream or Nano Banana, anything that returns a file URL. Configure it like this:

SettingValue
MethodPOST
URLhttps://curlyflies.com/v1/upload-url
AuthenticationGeneric Credential Type → Header Auth
Header nameAuthorization
Header valueBearer curly_live_...
Body Content TypeJSON
// JSON body — map the upstream node's file URL in
{
  "url": "{{ $json.image_url }}",
  "ttl_seconds": 604800
}
STEP 02

CurlyFlies fetches the file server-side and returns a public URL

No binary passes through n8n — CurlyFlies downloads the source URL itself while it’s still valid and re-hosts the bytes. The node’s output:

{
  "file_id": "x7k2p9",
  "url": "https://curlyflies.com/f/x7k2p9.png",
  "expires_at": "2026-06-19T09:41:00Z",
  "size_bytes": 204800,
  "content_type": "image/png",
  "delete_token": "dt_abc123"
}
STEP 03

Reference {{ $json.url }} in your publish node

In your Instagram (Facebook Graph API) node, Buffer node, or any downstream HTTP call, use {{ $json.url }} from the CurlyFlies node as the image_url / media URL. The crawler fetches it, gets 200 + image/png + raw bytes, and accepts the post. Check expires_at if you persist URLs — set ttl_seconds longer than your furthest scheduled slot.

Free plan, honestly: 100 uploads/month, 5MB max per file, and a fixed 24-hour TTL — plenty for testing and same-day publishing. Posts scheduled more than a day out need a paid plan: Builder gives TTLs up to 30 days, Pro 60, Scale 90. Plans →

Where it sits in the workflow.

One node between generation and publishing. Everything upstream and downstream stays exactly as it is.

GENERATE

GPT Image / Nano Banana / Seedream / Replicate
→ signed URL, dies in ~1h

CURLYFLIES

HTTP Request node
POST /v1/upload-url → stable public URL

PUBLISH

Instagram Graph API / Buffer / schedule
image_url = {{ $json.url }}

This is the failure it fixes: generation APIs return temporary signed URLs (Azure blob links with se= expiry, S3 presigned URLs). Fast runs publish before expiry; runs with queues, retries, waits, or approval steps don’t — and Instagram answers with error 9004, “media fetch failed.” Scheduled posts make it worse: the URL must still be alive when the schedule fires, not just when the workflow runs. Re-host immediately after generation, set the TTL past your schedule, and the intermittent failures disappear. Full error-9004 guide →

Frequently asked questions.

How do I get a public URL for a file in n8n?

One HTTP Request node: POST https://curlyflies.com/v1/upload-url with Header Auth (Authorization: Bearer curly_live_...) and a JSON body of {"url": "{{ $json.image_url }}", "ttl_seconds": 604800}. The response’s url field is a clean public URL that serves raw bytes with the correct Content-Type.

Why does my n8n Instagram workflow fail with error 9004?

Almost always an expired or non-public media URL. Generation APIs return signed URLs that die within roughly an hour, and Drive/Dropbox links serve HTML viewer pages instead of the file. Instagram’s crawler fetches your image_url itself, anonymously — if it can’t get raw bytes, you get 9004. Troubleshooting guide →

What TTL should I set for scheduled posts?

Longer than your furthest-out slot, plus margin. A weekly calendar → "ttl_seconds": 604800 (7 days). You can also send an absolute expires_at datetime instead, which matches how schedulers think. Free plan TTL is fixed at 24h; longer TTLs need Builder (30d), Pro (60d), or Scale (90d).

Can n8n upload a binary file directly instead of a URL?

Yes. Use POST https://curlyflies.com/v1/upload with Body Content Type Form-Data and the binary property mapped to the file field. Same auth header, same response shape. upload-url is usually simpler because n8n never has to hold the bytes.

How is this different from hosting on S3 or Drive from n8n?

Drive/Dropbox share links fail crawler validation outright (they serve HTML). S3 works once you’ve configured buckets, policies, and Content-Type metadata — about 45 minutes of AWS setup. CurlyFlies is one node and the files clean themselves up after the TTL. See the full comparison →

Test it before you build the workflow.

Same call your HTTP Request node will make — try it from your terminal first.

$ curl -X POST https://curlyflies.com/v1/upload-url \
    -H "Authorization: Bearer $CURLY_KEY" \
    -H "Content-Type: application/json" \
    -d '{"url": "https://...signed-url...", "ttl_seconds": 604800}'

→ { "url": "https://curlyflies.com/f/x7k2p9.png",
    "expires_at": "2026-06-19T09:41:00Z" }