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_DIRorWATCH_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
-
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_TOKENin your tr-engine.envand use that token:AUTH_TOKEN=your-secret-upload-token -
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 } ] } -
Restart trunk-recorder. Calls will start appearing in tr-engine.
Authentication
The upload endpoint checks credentials in this order:
Authorization: Bearer <token>header?token=<token>query parameterkeyform field (rdio-scanner convention)api_keyform field (OpenMHz convention)
Which credential to use:
| Auth mode | Upload authenticates with | Web UI / read API uses |
|---|---|---|
| Open mode | No credential required | No credential required |
| Token mode | AUTH_TOKEN | AUTH_TOKEN entered by the user |
| Full mode | API key (tre_...) or JWT bearer token | Public read token or JWT session |
| Legacy write-token mode | WRITE_TOKEN | AUTH_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
| Field | rdio-scanner | OpenMHz |
|---|---|---|
System name (systemLabel) | yes | no |
| Talkgroup alpha tag | yes | no |
| Talkgroup description | yes | no |
| Talkgroup tag (category) | yes | no |
| Talkgroup group | yes | no |
| Audio file | yes | yes |
| Source list (unit IDs) | yes | yes |
| Frequency list (hops) | yes | yes |
| Start time | yes | yes |
| Stop time | no | yes |
| Emergency flag | yes | yes |
| Encrypted flag | yes | no |
| Audio type (m4a/wav) | yes | no |
| Error count | no | yes |
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, orsystemLabelfields - OpenMHz: identified by
callortalkgroup_numfields
Sample Configurations
rdio-scanner Plugin (Recommended)
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
- trunk-recorder finishes recording a call
- The upload plugin POSTs the audio file + metadata as a multipart form to
/api/v1/call-upload - tr-engine auto-detects the format, parses the metadata, and authenticates via the form
key/api_keyfield - 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
- Returns
201 Createdwith the call ID, or409 Conflictif the call is a duplicate
Responses
| Status | Meaning |
|---|---|
201 Created | Call ingested successfully. Response body contains call_id, system_id, tgid, start_time. |
400 Bad Request | Invalid multipart form, unrecognized format, or missing required fields. |
401 Unauthorized | Missing or invalid auth token. |
409 Conflict | Duplicate call (same system, talkgroup, and start time within 5 seconds). |
413 Request Too Large | Upload exceeds the 50 MB limit. |
Environment Variables
| Variable | Default | Description |
|---|---|---|
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_ID | http-upload | Instance ID assigned to uploaded calls for identity resolution. |