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.
One Webhooks by Zapier action, configured once. No app integration to install, no OAuth dance.
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:
| Setting | Value |
|---|---|
| App & event | Webhooks by Zapier → Custom Request |
| Method | POST |
| URL | https://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 }
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"
}
Url field in your publish stepIn 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.
One step between generation and publishing. Trigger, delays, filters, and the publish step all stay as they are.
GPT Image / Nano Banana / Seedream / Replicate→ signed URL, dies in ~1h
Webhooks by Zapier · Custom RequestPOST /v1/upload-url → stable public URL
Instagram for Business / Buffer / Graph APIphoto 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 →
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.
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 →
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).
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.
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 →
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" }