HTTP Call Upload

May 5, 2026 · View on GitHub

tr-engine can ingest calls via HTTP upload, compatible with trunk-recorder's rdio-scanner and OpenMHz upload plugins. This is useful when:

  • You don't have local access to trunk-recorder's audio directory (no TR_DIR or WATCH_DIR)
  • You're already uploading to another service (OpenMHz, Broadcastify) and want to add tr-engine
  • trunk-recorder runs on a different machine with no shared filesystem or MQTT broker

Quick Setup

  1. Create an upload credential in tr-engine.

    Recommended for new installs: enable full auth with an admin password, then create an API key from the admin UI or API. Use that API key as the upload plugin credential.

    Legacy token mode is still supported. If you have not enabled full auth, set AUTH_TOKEN in your tr-engine .env and use that token:

    AUTH_TOKEN=your-secret-upload-token
    
  2. Add the rdio-scanner plugin to your trunk-recorder config.json:

    {
      "name": "rdioscanner_uploader",
      "library": "librdioscanner_uploader.so",
      "server": "https://your-tr-engine.example.com/api/v1/call-upload",
      "systems": [
        {
          "shortName": "butco",
          "apiKey": "your-secret-upload-token",
          "systemId": 1
        }
      ]
    }
    
  3. Restart trunk-recorder. Calls will start appearing in tr-engine.

Authentication

The upload endpoint checks credentials in this order:

  1. Authorization: Bearer <token> header
  2. ?token=<token> query parameter
  3. key form field (rdio-scanner convention)
  4. api_key form field (OpenMHz convention)

Which credential to use:

Auth modeUpload authenticates withWeb UI / read API uses
Open modeNo credential requiredNo credential required
Token modeAUTH_TOKENAUTH_TOKEN entered by the user
Full modeAPI key (tre_...) or JWT bearer tokenPublic read token or JWT session
Legacy write-token modeWRITE_TOKENAUTH_TOKEN

For new public-facing installs, prefer full mode: set ADMIN_PASSWORD, log in, and create a service API key for trunk-recorder uploads. WRITE_TOKEN remains accepted for backward compatibility, but it is deprecated.

Choosing a Plugin

Recommendation: Use the rdio-scanner plugin. It sends significantly more metadata per upload.

Metadata Comparison

Fieldrdio-scannerOpenMHz
System name (systemLabel)yesno
Talkgroup alpha tagyesno
Talkgroup descriptionyesno
Talkgroup tag (category)yesno
Talkgroup groupyesno
Audio fileyesyes
Source list (unit IDs)yesyes
Frequency list (hops)yesyes
Start timeyesyes
Stop timenoyes
Emergency flagyesyes
Encrypted flagyesno
Audio type (m4a/wav)yesno
Error countnoyes

With the rdio-scanner plugin, talkgroup names and tags show up immediately in tr-engine without needing a CSV import or TR_DIR auto-discovery. With OpenMHz, you'll only see raw talkgroup IDs until talkgroups are populated from another source.

Format Auto-Detection

tr-engine auto-detects the upload format from the form field names — no configuration needed on the tr-engine side:

  • rdio-scanner: identified by audio, audioName, or systemLabel fields
  • OpenMHz: identified by call or talkgroup_num fields

Sample Configurations

Add to the plugins array in trunk-recorder's config.json:

{
  "name": "rdioscanner_uploader",
  "library": "librdioscanner_uploader.so",
  "server": "https://your-tr-engine.example.com/api/v1/call-upload",
  "systems": [
    {
      "shortName": "butco",
      "apiKey": "your-secret-upload-token",
      "systemId": 1
    }
  ]
}

The shortName must match the system's shortName in your trunk-recorder config. The apiKey is your tr-engine API key (tre_...) in full auth mode, or AUTH_TOKEN in token mode. The deprecated WRITE_TOKEN also works during the transition. The systemId is sent but not used by tr-engine — identity resolution uses shortName instead.

Multiple systems:

{
  "name": "rdioscanner_uploader",
  "library": "librdioscanner_uploader.so",
  "server": "https://your-tr-engine.example.com/api/v1/call-upload",
  "systems": [
    { "shortName": "butco", "apiKey": "your-secret-upload-token", "systemId": 1 },
    { "shortName": "warco", "apiKey": "your-secret-upload-token", "systemId": 2 }
  ]
}

OpenMHz Plugin

Add to the plugins array in trunk-recorder's config.json:

{
  "name": "openmhz_uploader",
  "library": "libopenmhz_uploader.so"
}

And set the upload server and per-system API keys at the top level and system level:

{
  "uploadServer": "https://your-tr-engine.example.com/api/v1/call-upload",
  "systems": [
    {
      "shortName": "butco",
      "apiKey": "your-secret-upload-token",
      ...
    }
  ]
}

Note: OpenMHz uses a single uploadServer for all systems, configured at the root of config.json.

Running Alongside Other Upload Services

trunk-recorder's plugin system loads each entry in the plugins array independently. You can run multiple upload plugins simultaneously — the same .so library can even be loaded twice with different configurations.

Example: Upload to both OpenMHz and tr-engine

If you're already sending to OpenMHz/Broadcastify and want to add tr-engine, keep your existing OpenMHz plugin and add the rdio-scanner plugin:

{
  "uploadServer": "https://api.openmhz.com",
  "plugins": [
    {
      "name": "openmhz_uploader",
      "library": "libopenmhz_uploader.so"
    },
    {
      "name": "rdioscanner_uploader",
      "library": "librdioscanner_uploader.so",
      "server": "https://your-tr-engine.example.com/api/v1/call-upload",
      "systems": [
        { "shortName": "butco", "apiKey": "your-tr-engine-write-token", "systemId": 1 }
      ]
    }
  ],
  "systems": [
    {
      "shortName": "butco",
      "apiKey": "your-openmhz-api-key",
      ...
    }
  ]
}

Each call is uploaded to both services independently. The apiKey in the systems array goes to OpenMHz; the apiKey in the rdio-scanner plugin config goes to tr-engine.

How It Works

  1. trunk-recorder finishes recording a call
  2. The upload plugin POSTs the audio file + metadata as a multipart form to /api/v1/call-upload
  3. tr-engine auto-detects the format, parses the metadata, and authenticates via the form key/api_key field
  4. The call goes through the standard ingest pipeline: identity resolution (auto-creates systems/sites), dedup check, call record creation, audio file storage, source/frequency processing, unit upserts, SSE event publishing, and transcription enqueue
  5. Returns 201 Created with the call ID, or 409 Conflict if the call is a duplicate

Responses

StatusMeaning
201 CreatedCall ingested successfully. Response body contains call_id, system_id, tgid, start_time.
400 Bad RequestInvalid multipart form, unrecognized format, or missing required fields.
401 UnauthorizedMissing or invalid auth token.
409 ConflictDuplicate call (same system, talkgroup, and start time within 5 seconds).
413 Request Too LargeUpload exceeds the 50 MB limit.

Environment Variables

VariableDefaultDescription
AUTH_TOKEN(empty)Shared token for token-mode deployments. In full mode, this can act as a public read token.
ADMIN_PASSWORD(empty)Enables full auth mode and JWT/API-key based write access. Recommended for public-facing deployments.
WRITE_TOKEN(empty)Deprecated legacy write token. Still accepted during the transition; prefer API keys.
UPLOAD_INSTANCE_IDhttp-uploadInstance ID assigned to uploaded calls for identity resolution.