Tonemap is the media API between your render farm and the review room. Submit an EXR sequence; get back colour-managed proxies, a thumbnail, a contact sheet and a QC report — through your OCIO config, not ours.
H.264 at the size and rate you ask for. Plays in a browser, RV, ShotGrid, ftrack, Kitsu.
mp4 · 1920 × 1080 · 24 fpsA poster frame — the one you name, not the first one.
jpg · frame 1050The whole range at a glance, columns by rows.
jpg · 5 × 4One strip for a scrubber, with the frame offsets as JSON beside it.
jpg · json · 100 framesThe verdict, machine-readable: every check, what it measured, and the frames it names.
jsonSigned, retried with back-off, replayable by event id. Your tracker hears about it before anyone asks.
media_job.completedEvery job names its input colour space, display and view. Tonemap resolves them against the config you registered, pins it by hash, and reports the config and LUT hashes beside the output — what was applied, not what was asked for.
Tonemap walks the range before it encodes a frame. Holes, duplicates, unreadable files and black frames are pinned to the frame number and written into the report, and the job’s events carry the count.
REST, a typed Python SDK, idempotency keys, signed webhooks. It goes in the farm’s post-job hook and nobody thinks about it again.
from tonemap import ( ArtifactType, ColorRequest, MediaJobCreate, OutputRequest, SourceAssetCreate, SourceType, TonemapClient, ) with TonemapClient("https://tonemap-api.fly.dev", os.environ["TONEMAP_API_KEY"]) as client: asset = client.create_source_asset(SourceAssetCreate( source_type=SourceType.SEQUENCE, frame_pattern="sq010_sh020_comp_v003.%04d.exr", frame_start=1001, frame_end=1100, )) job = client.create_media_job(MediaJobCreate( source_asset_id=asset.id, idempotency_key="salt-sq010-sh020-comp-v003", color=ColorRequest( mode="ocio", ocio_config_id="ocio_aces_1_3_salt", input_color_space="ACEScg", display="sRGB", view="ACES 1.0 - SDR Video", ), outputs=( OutputRequest(type=ArtifactType.PROXY, container="mp4", codec="h264"), OutputRequest(type=ArtifactType.THUMBNAIL, frame=1050), OutputRequest(type=ArtifactType.CONTACT_SHEET, columns=5, rows=4), OutputRequest(type=ArtifactType.QC_REPORT), ), )) done = client.wait_for_media_job(job.id) # or don’t — take the webhook
curl https://tonemap-api.fly.dev/v1/media-jobs \ -H "Authorization: Bearer $TONEMAP_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "source_asset_id": "src_7Hq2mX", "idempotency_key": "salt-sq010-sh020-comp-v003", "metadata": { "project": "SALT", "sequence": "sq010", "shot": "sh020", "task": "comp", "version": "v003", "frame_start": 1001, "frame_end": 1100 }, "color": { "mode": "ocio", "ocio_config_id": "ocio_aces_1_3_salt", "input_color_space": "ACEScg", "display": "sRGB", "view": "ACES 1.0 - SDR Video" }, "outputs": [ { "type": "proxy", "container": "mp4", "codec": "h264" }, { "type": "thumbnail", "frame": 1050 }, { "type": "contact_sheet", "columns": 5, "rows": 4 }, { "type": "qc_report" } ] }'
# POST to the endpoint you registered # Tonemap-Webhook-Id: evt_01J9ZK4T # Tonemap-Webhook-Timestamp: 2026-09-17T14:02:19+00:00 # Tonemap-Webhook-Signature: v1=5f0c…b81e { "event_type": "media_job.completed", "media_job_id": "mj_4Tn8cVwR", "status": "completed", "artifact_count": 4, "qc_finding_count": 1, "artifacts": [ { "id": "art_p91x", "type": "proxy", "width": 1920, "height": 1080, "duration": 4.167 }, { "id": "art_c07k", "type": "contact_sheet" } ] }
Tonemap is not where your masters live. It holds media for as long as processing needs it, and every API response says what is held and until when.
No notes, playlists, approvals or sync sessions. Tonemap feeds the tools that already do that well, and is judged on one thing: the picture that arrives is the picture you rendered.
Tell us what your farm writes and what your review tool reads. We set up your studio with you and send an invite bound to your address.
No newsletter. One reply, from a person who has shipped shots.