Use CurlyFlies with Zapier

One webhook step.
A URL that survives the Zap.

Your AI step returns a signed URL that expires in about an hour — before your Delay step, your retry, or your Tuesday-9am schedule fires. One Webhooks by Zapier action turns it into a stable public URL that serves raw bytes for days.

The recipe.

One Webhooks by Zapier action, configured once. No app integration to install, no OAuth dance.

STEP 01

Add a Webhooks by Zapier action after your generation step

Right after the step that produces the image — OpenAI (GPT Image), Replicate, or any AI step that outputs a file URL — add Webhooks by Zapier with the Custom Request event:

SettingValue
App & eventWebhooks by Zapier → Custom Request
MethodPOST
URLhttps://curlyflies.com/v1/upload-url
Data Pass-Through?False
Headers (row 1)Authorization  ·  Bearer curly_live_...
Headers (row 2)Content-Type  ·  application/json
// Data field — insert the image URL from the previous step
// where the {{...}} placeholder is
{
  "url": "{{Image URL from step 1}}",
  "ttl_seconds": 604800
}
STEP 02

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

No file passes through Zapier — CurlyFlies downloads the source URL while it’s still valid and re-hosts the bytes. Test the step and Zapier parses the response into mappable fields:

{
  "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

Use the returned Url field in your publish step

In your Instagram for Business action, Buffer action, or a second Custom Request to the Graph API, click the photo/media URL field and pick Url from the CurlyFlies webhook step’s output. The crawler fetches it, gets 200 + image/png + raw bytes, and accepts the post. If a Delay step or schedule sits between, make sure ttl_seconds outlives it — Expires At from the output tells you the deadline.

Free plan, honestly: 100 uploads/month, 5MB max per file, and a fixed 24-hour TTL — fine for testing and same-day Zaps. 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 Zap.

One step between generation and publishing. Trigger, delays, filters, and the publish step all stay as they are.

GENERATE

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

CURLYFLIES

Webhooks by Zapier · Custom Request
POST /v1/upload-url → stable public URL

PUBLISH

Instagram for Business / Buffer / Graph API
photo URL = Url from webhook step

This is the failure it fixes: generation APIs hand back temporary signed URLs (Azure blob links with se= expiry, S3 presigned URLs). A Zap that runs end-to-end in seconds works; add a Delay step, a queue of retries, or schedule the post for later in the week, and the URL is dead when Instagram’s crawler fetches it — error 9004, “media fetch failed.” Re-host immediately after generation with a TTL longer than the schedule, and the intermittent failures disappear. Full error-9004 guide →

Frequently asked questions.

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

One Webhooks by Zapier action with the Custom Request event: POST https://curlyflies.com/v1/upload-url, headers Authorization: Bearer curly_live_... and Content-Type: application/json, and a JSON body with the source url and a ttl_seconds. The returned url serves raw bytes with the correct Content-Type and is mappable in every later step.

Why does my Zapier Instagram Zap 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 media 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 I use the simpler “POST” webhook event instead of Custom Request?

Custom Request is the reliable choice because it gives you full control of headers and the raw JSON body. The basic POST event form-encodes data by default; if you use it, set Payload Type to json and add the Authorization header — but Custom Request avoids the foot-guns.

Why not just point Instagram at a Drive/Dropbox link or S3?

Drive and Dropbox share links serve HTML viewer pages, which fail crawler validation outright. S3 works after you configure buckets, public-read policies, and Content-Type metadata — roughly 45 minutes of AWS setup. CurlyFlies is one webhook step, and files delete themselves after the TTL. See the full comparison →

Test it before you build the Zap.

Same request your Custom Request step 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" }