Reference

Delivery API

Everything Framehaul serves is addressed by URL. There is no SDK to install and no job to submit: you ask for a rendition and the first request produces it.

URL structure

A delivery URL has four parts: your edge hostname, the API version, a comma-separated list of parameters, and the path of the original as you uploaded it.

https://q8vd2mr6ha.edge.framehaul.com/v1/w=1200,fit=cover,fmt=webp/library/harbour-crane.jpg
       └────────── 1 ──────────────┘ └2┘ └────────── 3 ─────────┘ └────────── 4 ─────────┘

Parameters are order independent, so w=800,q=70 and q=70,w=800 resolve to the same cache entry. An unrecognised key is rejected with 400 rather than ignored, because silently dropping a parameter is how you end up shipping 4 MB images without noticing.

Edge hostnames

Each account is issued its own edge hostname. The label is generated, not sequential, and it can change when capacity is rebalanced, so treat it as opaque:

If you need a hostname of your own instead, a CNAME onto your domain works and is included on every plan. Certificates are issued and renewed for you.

Image parameters

KeyValuesDefaultNotes
w1 to 8192source widthTarget width in pixels.
h1 to 8192derivedOmit one of w or h to keep the aspect ratio.
fitcover, contain, fill, insidecoverHow the source is mapped onto the requested box.
poscenter, top, bottom, left, right, attncenterCrop anchor. attn picks the busiest region.
dpr1 to 41Multiplies w and h. Cheaper than doing the arithmetic yourself.
q1 to 10078Ignored for lossless output.
fmtavif, webp, jpeg, png, autoautoSee format negotiation.
bghex, or transparenttransparentFill behind fit=contain.
blur0.3 to 100offGaussian sigma. Useful for placeholders.
sharpen0 to 100Applied after resampling.
rotauto, 90, 180, 270autoauto honours the EXIF orientation, then strips it.
metanone, copyright, allnoneWhich metadata survives into the output.
Metadata is dropped by default. Camera originals routinely carry GPS coordinates, and user uploads carry them more often than anyone expects. Ask for meta=all deliberately, not by habit.

Video parameters

The same URL shape addresses video. Ask for a still and you get an image back; ask for a rendition or a playlist and you get video.

KeyValuesDefaultNotes
tseconds, or hh:mm:ss.ms-Extract a still at this timestamp. Combines with every image parameter.
vcodech264, h265, autoautoPer-request codec for progressive output.
vbr200k to 20MderivedTarget video bitrate. Derived from height when omitted.
fps1 to 60sourceFrames are dropped, never interpolated.
audiokeep, dropkeepDropping audio is worth it for silent background loops.
clipstart,endwhole fileTrim without re-uploading.

Adaptive playlists

Request /hls/master.m3u8 against a source path and Framehaul builds a ladder from the master. Renditions are produced on demand, so the first viewer of an unusual rung waits for it while everyone after them does not.

https://q8vd2mr6ha.edge.framehaul.com/v1/hls/master.m3u8/library/opening-titles.mov

The default ladder is 360p, 540p, 720p and 1080p, capped at the master's own height. Pass rungs=540,1080 to narrow it.

Format negotiation

With fmt=auto, which is the default, output is chosen from the request's Accept header: AVIF where it is offered, then WebP, then JPEG. Every variant is cached separately and Vary: Accept is set, so a shared cache in front of us stays correct.

Pin the format when the URL ends up somewhere that strips headers. Email clients and some social scrapers do exactly that, and they are the usual reason a picture arrives broken.

Caching

Derived output is immutable: a given URL always describes the same bytes, so it is cached for a year at the edge and in the browser. Replacing an original is what invalidates its derivatives, not time passing.

HeaderMeaning
x-cacheHIT, MISS, or BYPASS when a parameter forbids caching.
cache-controlpublic, max-age=31536000, immutable on derived output.
etagDerived from the original plus the full parameter set.
x-request-idQuote this when you write to support. It is the fastest way to find one request.

Purging

Uploading over a path purges everything derived from it automatically. Purge by hand when you have deleted an original or want a prefix gone.

# one path and everything derived from it
curl -X POST https://api.framehaul.com/v1/purge \
  -H "Authorization: Bearer $FRAMEHAUL_TOKEN" \
  -d '{"path":"/library/harbour-crane.jpg"}'

# a whole prefix
curl -X POST https://api.framehaul.com/v1/purge \
  -H "Authorization: Bearer $FRAMEHAUL_TOKEN" \
  -d '{"prefix":"/library/2024-campaign/"}'

Purges are acknowledged immediately and applied across the edge within a few seconds. Prefix purges are rate limited more tightly than single paths.

Signed URLs

Signing is off by default, since most libraries are meant to be public. Turn it on per account and unsigned requests stop being served.

message   = "?&exp="
signature = base64url(hmac_sha256(signing_key, message))
url       = "//?exp=&sig="

Parameters are sorted before signing so that reordering them does not change the signature. Expired links return 410, not 403, so your logs distinguish a link that aged out from one that was tampered with.

Signing keys are shown once. We store a hash, so a lost key is rotated rather than recovered. Two keys can be live at a time, which is what makes rotation possible without downtime.

Uploading

Upload straight to the API for anything under 200 MB. Larger masters get a resumable session.

# direct
curl -X PUT https://api.framehaul.com/v1/objects/library/harbour-crane.jpg \
  -H "Authorization: Bearer $FRAMEHAUL_TOKEN" \
  -H "Content-Type: image/jpeg" \
  --data-binary @harbour-crane.jpg

# resumable, for large video masters
curl -X POST https://api.framehaul.com/v1/uploads \
  -H "Authorization: Bearer $FRAMEHAUL_TOKEN" \
  -d '{"path":"/library/opening-titles.mov","size":8419923456}'

The resumable response carries a session URL that accepts Content-Range chunks and survives a dropped connection for 24 hours. Browser uploads should go through a short lived upload token from your own backend, so the account token never reaches a client.

Errors

Errors are JSON, never a placeholder image. Serving a grey rectangle with 200 hides the problem until someone notices it in production.

HTTP/2 404
content-type: application/json

{"error":"not_found","request_id":"9f4c1ab0e7d24b8e"}
StatusCodeMeaning
400bad_parameterUnknown key, or a value out of range. The message names the key.
403bad_signatureSignature missing or does not match.
404not_foundNo original at that path.
410link_expiredSigned link past its exp.
415unsupported_sourceThe original is not a media type we decode.
422source_damagedThe file decodes partially. The original is kept so you can inspect it.
429rate_limitedCheck retry-after. Applies to the API, not to delivery.
503transform_busyTransform capacity is saturated. Retry with backoff.

Limits

LimitValue
Original image120 MB, up to 20000 px on the long side
Original video40 GB, up to 4 hours
Output image8192 px on either side
Parameters per URL16
API requests60 per minute, bursting to 120
Prefix purges10 per hour
DeliveryNot rate limited. That is the part you pay for.

If a limit is in your way, write to support@framehaul.com and say what you are trying to do. Most of them are conservative defaults rather than hard ceilings.