Docs

Hosted Sessions

AI pipeline integration guide: see /llms.txt for a condensed, LLM-friendly version of this document.

audiocutter.online accepts a hosted session — a JSON project manifest hosted at any URL, opened by visiting https://audiocutter.online/?session=<URL>. After the user edits the project in the browser, the app posts the edited manifest and rendered audio back to a callback URL declared in the manifest.

This unlocks integrations like:

The same ProjectManifest schema also powers the local .zip save/load feature. The only difference: in a hosted session, audio file references are remote (https:// URLs or inline data: URIs) instead of relative paths inside a ZIP.


Quick start

Build a project manifest. The live demo manifest served from /demo-session.json is shown below — copy it, adjust the source / callbackUrl / branding to taste, and host it anywhere with appropriate CORS headers (see CORS requirements):

{
  "version": 2,
  "project": {
    "volume": 0.9,
    "fadeIn": {
      "duration": 2000000
    },
    "fadeOut": {
      "duration": 3000000
    },
    "compression": {
      "enabled": true,
      "threshold": -20,
      "ratio": 3,
      "knee": 20,
      "attack": 0.005,
      "release": 0.3,
      "makeupGain": 3
    },
    "filter": {
      "enabled": true,
      "type": "highpass",
      "frequency": 60,
      "q": 0.707
    },
    "files": [
      {
        "id": "file-1",
        "filename": "funky-boogie-brothers-the-little-big-beat-track.mp3",
        "source": "https://cdn.audiocutter.online/sounds/funky-boogie-brothers-the-little-big-beat-track.mp3"
      }
    ],
    "fragments": [
      {
        "id": "demo-fragment-intro",
        "name": "Intro",
        "start": 0,
        "fileId": "file-1",
        "fileStart": 0,
        "fileEnd": 6000000,
        "volume": 0.8,
        "fadeIn": 0,
        "fadeOut": 1000000
      },
      {
        "id": "demo-fragment-main",
        "name": "Main Beat",
        "start": 5000000,
        "fileId": "file-1",
        "fileStart": 20000000,
        "fileEnd": 35000000,
        "volume": 1,
        "fadeIn": 1000000,
        "fadeOut": 1000000
      },
      {
        "id": "demo-fragment-break",
        "name": "Break",
        "start": 19000000,
        "fileId": "file-1",
        "fileStart": 95000000,
        "fileEnd": 110000000,
        "volume": 0.75,
        "fadeIn": 1000000,
        "fadeOut": 1000000
      },
      {
        "id": "demo-fragment-finale",
        "name": "Finale",
        "start": 33000000,
        "fileId": "file-1",
        "fileStart": 120000000,
        "fileEnd": 135000000,
        "volume": 1,
        "fadeIn": 1000000,
        "fadeOut": 0
      }
    ]
  },
  "session": {
    "ui": {
      "title": "Hosted session demo",
      "instruction": "This is what an integration looks like. Edit the remix, then Submit to echo the payload back via httpbin.org.",
      "submitLabel": "Submit (demo)",
      "accentColor": "#7c3aed"
    },
    "output": {
      "mode": "multipart",
      "callbackUrl": "https://httpbin.org/post",
      "format": "mp3"
    }
  }
}

Most fields have sensible defaults (see Defaults) — the bare minimum is a file source/id and a fragment with id/fileId. Omitted fileEnd means "the whole file". version, project volume/fades, compression, filter, the fragment's remaining fields — all defaulted.

Open https://audiocutter.online/?session=/demo-session.json to load the demo as a live session — banner with the partner accent color, audio fetched from the CDN, Submit echoes to httpbin.org/post. Your own manifests work the same way: open https://audiocutter.online/?session=<URL-to-your-manifest>.


URL parameter contract

ParamTypeDescription
sessionhttps:// URLAbsolute URL pointing to a manifest JSON document. Must be URL-encoded if it has special characters.

Only one parameter is recognized; everything else is reserved for future use. A request without ?session= opens the editor normally.


Manifest schema

The hosted-session manifest is the same shape as the local-ZIP project manifest. The two extension points for hosted sessions are:

  1. The session block (optional) carries branding and submit-back config.
  2. Each project.files[].source value is one of three forms (see below).

Top-level

{
  version: 2,                   // PROJECT_FILE_VERSION
  project: { ... },             // see "Project" below
  session?: {
    ui?: { ... },               // optional branding
    output?: { ... }            // omit to disable submit-back; user falls back to local download
  }
}

Project

{
  volume: 0..1,
  fadeIn:  { duration: number },        // microseconds
  fadeOut: { duration: number },        // microseconds
  compression: { enabled, threshold, ratio, knee, attack, release, makeupGain },
  filter:      { enabled, type: 'lowpass'|'highpass'|'bandpass'|'notch', frequency, q },
  files: Array<{
    id: string,
    source: string,                      // see "source forms" below
    filename?: string            // optional only when source is a data: URI; required for URL / path
  }>,
  fragments: Array<{
    id: string,
    name: string,
    start: number,                       // microseconds, position on timeline
    fileId: string,                      // references files[].id
    fileStart: number,                   // microseconds, slice start within the source file
    fileEnd?: number,                    // microseconds, slice end. Omit → whole file.
    volume: 0..1,
    fadeIn: number,                      // microseconds
    fadeOut: number                      // microseconds
  }>
}

Time values are stored in microseconds. 1 sec = 1_000_000.

Defaults

Most fields can be omitted; the loader fills them in. Required fields are the bare minimum to identify and place audio:

RequiredWhy
project.files[].idFragments reference files by id.
project.files[].sourceThe audio bytes — can't be defaulted.
project.fragments[].idIdentifies the fragment.
project.fragments[].fileIdBinds the fragment to a file.

Everything else has a default:

FieldDefault
version2
project.volume1
project.fadeIn.duration0
project.fadeOut.duration0
project.compression{ enabled: false, threshold: -24, ratio: 4, knee: 30, attack: 0.003, release: 0.25, makeupGain: 0 }
project.filter{ enabled: false, type: 'lowpass', frequency: 350, q: 1 }
project.files[].filenamederived from MIME (only when source is a data: URI); required otherwise
project.fragments[].name""
project.fragments[].start0
project.fragments[].fileStart0
project.fragments[].fileEndfull file duration (resolved at load time)
project.fragments[].volume1
project.fragments[].fadeIn0
project.fragments[].fadeOut0

source forms

source is checked against three forms in order:

FormExampleResolution
data: URI (base64)data:audio/mpeg;base64,SUQzBAA...Decoded inline. Self-contained. No CORS hop. ~33% size overhead — best for short clips.
https:// URL (absolute)https://cdn.example.com/clip.mp3Fetched as-is. Requires CORS-friendly headers (see below).
Relative pathaudio/clip.mp3 or /audio/clip.mp3In a hosted session: resolved against the manifest URL with new URL(source, manifestUrl) — so a manifest at https://cdn.example.com/sessions/abc.json with source: "audio/clip.mp3" fetches https://cdn.example.com/sessions/audio/clip.mp3. In a local ZIP: looked up inside the ZIP.

Only https:// is honored for absolute network URLs — http:// is rejected to avoid mixed-content and downgrade attacks. (Relative paths inherit the manifest URL's scheme — so an http://localhost manifest can serve http://localhost audio for local development, because browsers treat localhost as a secure context.)

session.ui

FieldTypeDescription
titlestringBanner title. Defaults to "Editing in a hosted session".
instructionstringSmall muted text under the title.
submitLabelstringSubmit button label. Defaults to "Submit".
accentColorCSS colorTints the banner background and Submit button.
logoUrlhttps:// URLImage displayed on the left of the banner. Must be CORS-friendly.

All fields are rendered as plain text — no HTML/Markdown. JavaScript can never be injected via these fields.

session.output

Three modes are supported. Choose mode to pick the dispatch strategy.

mode: 'multipart' (recommended for most pipelines)

One POST with the full payload as multipart/form-data.

FieldTypeDescription
mode"multipart"Required.
callbackUrlhttps:// URLRequired. Receives the POST.
formataudio extensionmp3, wav, ogg, mp4, flac, aac, aiff. Defaults to wav.
headersRecord<string,string>Sent on the POST. Use for Authorization.
successRedirectUrlhttps:// URLIf set, the browser navigates here after success.

The POST body contains two parts:

mode: 'split'

Audio is uploaded first, manifest second. Use when your audio store and metadata store are separate services.

The audio upload response body is forwarded into the manifest POST as the X-Audio-Upload-Response header (stringified). Use this to correlate the two requests server-side.

mode: 'presigned'

Audio is uploaded directly to a pre-signed URL (e.g. S3 PUT), manifest sent to your service.


CORS requirements

audiocutter.online is served with strict cross-origin headers (Cross-Origin-Opener-Policy: same-origin, Cross-Origin-Embedder-Policy: require-corp) so it can use SharedArrayBuffer for FFmpeg-based audio conversion. As a result, every cross-origin resource the page fetches must opt in.

For each https:// resource you serve:

data: URI sources bypass CORS entirely.

Callback servers

For output.callbackUrl / output.manifestUrl / output.audioUploadUrl, the browser makes a cross-origin POST/PUT. Your server must respond to CORS preflight with:

For mode: 'presigned', your signed URL provider (S3, R2, etc.) must also allow cross-origin PUTs. AWS S3 CORS config example:

[
  {
    "AllowedOrigins": ["https://audiocutter.online"],
    "AllowedMethods": ["PUT"],
    "AllowedHeaders": ["Content-Type"],
    "MaxAgeSeconds": 3000
  }
]

Authentication

Use session.output.headers to attach Authorization (or any other) headers to the callback requests:

"output": {
  "mode": "multipart",
  "callbackUrl": "https://your.api/callback",
  "headers": { "Authorization": "Bearer eyJhbGciOi..." }
}

Security note: the manifest is fetched by the browser and is visible to anyone with the session URL. Do not embed long-lived secrets directly. Recommended patterns:


End-to-end recipes

Python (FastAPI) receiver — multipart mode

from fastapi import FastAPI, File, UploadFile
import json

app = FastAPI()

@app.post("/callback")
async def receive(manifest: UploadFile = File(...), audio: UploadFile = File(...)):
    manifest_data = json.loads(await manifest.read())
    audio_bytes = await audio.read()
    # ...persist, kick off the next pipeline stage, etc.
    return {"ok": True}

Node (Express + multer) receiver — multipart mode

import express from 'express'
import multer from 'multer'

const upload = multer()
const app = express()

app.post('/callback', upload.fields([{ name: 'manifest' }, { name: 'audio' }]), (req, res) => {
  const manifest = JSON.parse(req.files.manifest[0].buffer.toString())
  const audio = req.files.audio[0].buffer
  // ...persist
  res.json({ ok: true })
})

S3 presigned PUT example

  1. Pipeline backend generates a presigned PUT URL for audio/<session-id>.mp3 in your bucket.
  2. Backend writes the manifest with output.mode = 'presigned', audioUrl = <signed URL>, manifestUrl = https://your.api/callback.
  3. User edits and clicks submit. Audio lands in S3, then your /callback receives the manifest JSON. Correlate by inspecting the manifest's session fields or by including a session id in manifestUrl's query string.

Error handling

FailureUser sees
Manifest URL unreachable / CORS-failtoast.error("Failed to load session") with the underlying message. Browser CORS errors are opaque; check your headers.
Manifest fails schema validationtoast.error("Invalid session manifest: <field>: <message>"). Validate locally with the Zod schema before serving.
Audio fetch failstoast.error(...). Same CORS rules apply to every audio URL.
Submit fails (callback 4xx/5xx)toast.error(<message>) with a "Your edits are preserved" hint. The user can retry or download locally.

The local Save Project (Cmd/Ctrl+Shift+S) entry remains available during a hosted session, so a user can always download a ZIP of their work if your callback is broken.


Versioning

PROJECT_FILE_VERSION is a single integer that bumps on breaking schema changes:

VersionChange
1Initial release. Files used zipPath.
2zipPath renamed to source and generalized to accept data: / https:// / relative ZIP path. session block added.

The loader accepts both v1 (with zipPath) and v2 (with source) for backwards compatibility. Always emit v2 for new integrations.


Security considerations


Future work

These items are intentionally out of scope for the initial implementation but are tracked here:

Questions?

Have a question or need help with the integration? and we’ll get back to you.