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:
- Store it in configuration, never assemble it in code from an account ID.
- Do not parse it, and do not assume anything from its length or characters.
- Do not pin its address. It resolves to different hosts over time and by location.
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
| Key | Values | Default | Notes |
|---|---|---|---|
w | 1 to 8192 | source width | Target width in pixels. |
h | 1 to 8192 | derived | Omit one of w or h to keep the aspect ratio. |
fit | cover, contain, fill, inside | cover | How the source is mapped onto the requested box. |
pos | center, top, bottom, left, right, attn | center | Crop anchor. attn picks the busiest region. |
dpr | 1 to 4 | 1 | Multiplies w and h. Cheaper than doing the arithmetic yourself. |
q | 1 to 100 | 78 | Ignored for lossless output. |
fmt | avif, webp, jpeg, png, auto | auto | See format negotiation. |
bg | hex, or transparent | transparent | Fill behind fit=contain. |
blur | 0.3 to 100 | off | Gaussian sigma. Useful for placeholders. |
sharpen | 0 to 10 | 0 | Applied after resampling. |
rot | auto, 90, 180, 270 | auto | auto honours the EXIF orientation, then strips it. |
meta | none, copyright, all | none | Which metadata survives into the output. |
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.
| Key | Values | Default | Notes |
|---|---|---|---|
t | seconds, or hh:mm:ss.ms | - | Extract a still at this timestamp. Combines with every image parameter. |
vcodec | h264, h265, auto | auto | Per-request codec for progressive output. |
vbr | 200k to 20M | derived | Target video bitrate. Derived from height when omitted. |
fps | 1 to 60 | source | Frames are dropped, never interpolated. |
audio | keep, drop | keep | Dropping audio is worth it for silent background loops. |
clip | start,end | whole file | Trim 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.
| Header | Meaning |
|---|---|
x-cache | HIT, MISS, or BYPASS when a parameter forbids caching. |
cache-control | public, max-age=31536000, immutable on derived output. |
etag | Derived from the original plus the full parameter set. |
x-request-id | Quote 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.
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"}
| Status | Code | Meaning |
|---|---|---|
| 400 | bad_parameter | Unknown key, or a value out of range. The message names the key. |
| 403 | bad_signature | Signature missing or does not match. |
| 404 | not_found | No original at that path. |
| 410 | link_expired | Signed link past its exp. |
| 415 | unsupported_source | The original is not a media type we decode. |
| 422 | source_damaged | The file decodes partially. The original is kept so you can inspect it. |
| 429 | rate_limited | Check retry-after. Applies to the API, not to delivery. |
| 503 | transform_busy | Transform capacity is saturated. Retry with backoff. |
Limits
| Limit | Value |
|---|---|
| Original image | 120 MB, up to 20000 px on the long side |
| Original video | 40 GB, up to 4 hours |
| Output image | 8192 px on either side |
| Parameters per URL | 16 |
| API requests | 60 per minute, bursting to 120 |
| Prefix purges | 10 per hour |
| Delivery | Not 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.