Docs · Trio-Lumen API

The API documentation is available in English.

Timed labels for first-person video.

Trio-Lumen labels first-person (egocentric) video with timed activity and per-hand action segments. You submit a batch of videos, review the quote, and fetch one verified JSON result per video.

Getting started

  1. Sign up at platform.machinefi.com.
  2. Add credits to your balance in the console (Billing). Lumen and Trio-Spark share one prepaid balance; there is no subscription. See Billing.
  3. Create an API key with Lumen enabled. In the console (API Keys), check Trio-Lumen when you create the key (it is checked by default). The secret is shown once; store it on your server.
  4. Submit a batch with POST /batches (see Workflow), using presigned https URLs as inputs and the default hosted output, then poll GET /batches/{id} and fetch results with GET /items/{id}/result.
curl https://platform.machinefi.com/api/lumen/v1/batches \
  -H "Authorization: Bearer $TRIO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "task": "egocentric-actions",
    "inputs": [{ "url": "https://storage.example.com/raw/clip-001.mp4?X-Amz-Signature=..." }],
    "output": { "type": "hosted" }
  }'

Steps 1–4 are fully self-serve. Two options need us for now; contact us to set them up:

Authentication

Send your API key as a bearer token:

Authorization: Bearer tf_...

Create the key in the platform console with Trio-Lumen checked. A key can be enabled for Trio-Spark, Trio-Lumen, or both; this is fixed when the key is created. A key that is not enabled for Lumen gets 403 missing_scope: create a new key with Lumen checked (you can then revoke the old one). Keys created through the console API without a products list are enabled for Trio-Spark only. Keep keys server-side and never embed them in clients.

Workflow

1. Create a batch

Send your video inputs. The batch starts in preflight: we fetch and inspect every video and compute a quote.

2. Quote

3. Processing

Each video becomes an item and is processed independently. Items that finish with a verified result are succeeded and are charged on the video time covered, rounded up to whole minutes, with a minimum of one minute per item.

4. Results

Only succeeded items are charged. Skipped, failed, cancelled and needs_review items are not charged. An item in needs_review is checked by us. If we approve it, it becomes succeeded and is charged then.

Endpoints

PathMethodPurpose
/batchesPOSTCreate a batch (requires Idempotency-Key)
/batchesGETList your batches, newest first
/batches/{id}GETGet a batch
/batches/{id}/confirmPOSTConfirm a quote, or resume a batch blocked for funds
/batches/{id}/cancelPOSTCancel a batch
/batches/{id}/itemsGETList a batch's items
/items/{id}/resultGETGet a short-lived download link for an item's result
/schemaGETResult JSON Schema and available task versions

Create a batch

POST https://platform.machinefi.com/api/lumen/v1/batches

HeaderDescription
AuthorizationBearer followed by your API key. Required.
Idempotency-KeyRequired. 8–255 characters from A–Z a–z 0–9 . _ : -. Use a new key for every new batch.
Content-Typeapplication/json
{
  "task": "egocentric-actions",
  "inputs": [
    {
      "url": "https://storage.example.com/raw/clip-001.mp4?X-Amz-Signature=..."
    },
    {
      "source": "src_3f2a...",
      "prefix": "raw/2026-11/"
    }
  ],
  "max_amount_usd": "250",
  "output": {
    "type": "hosted"
  },
  "webhook_url": "https://hooks.example.com/lumen",
  "metadata": {
    "project": "warehouse-q4"
  }
}
FieldDescription
taskRequired. "egocentric-actions"
versionOptional. A task version from GET /schema. Defaults to "latest". The version used is pinned on the batch as processing_version.
inputsRequired. 1–2000 entries. Each is either { "url" } or { "source", "prefix" }. A batch holds at most 2000 videos in total: a prefix that lists more fails the batch with too_many_items.
inputs[].urlA public https URL we can GET, typically a presigned URL valid for at least 24 hours. We copy the video during preflight and do not keep the URL after that.
inputs[].source, inputs[].prefixA registered bucket (we give you its src_... id) and a folder inside its read scope. Every video under the prefix becomes an item. The listing is taken once during preflight.
max_amount_usdOptional. Auto-confirm quotes up to this amount. Omit it to confirm every non-zero quote yourself.
outputOptional. { "type": "hosted" } (default), or { "type": "bucket", "source": "src_...", "prefix": "lumen-out/" } to write results into your bucket. The prefix must be a folder inside the source's write scope.
webhook_urlOptional. A public https URL for webhooks. Contact us for a signing secret first.
metadataOptional. Your own key/values, up to 32 keys and 8 KB. Returned unchanged.

Response: 202 Accepted with the batch object.

Bucket output. Every batch writes into its own folder, <prefix><batch_id>/, so batches that share an output prefix never touch each other's files:

Idempotency.

Batch object

{
  "object": "batch",
  "id": "8d0c7c1e-4b0f-4f1a-9a57-0f3f6a0f2b11",
  "status": "running",
  "blocked_reason": null,
  "failure_reason": null,
  "cancel_requested": false,
  "task": "egocentric-actions",
  "processing_version": "egocentric-actions@<version>",
  "created_at": "2026-11-01T18:02:11.204Z",
  "confirmed_at": "2026-11-01T18:04:40.017Z",
  "finished_at": null,
  "preflight": {
    "pending_inputs": 0
  },
  "quote": {
    "amount_usd": "...",
    "price_per_hour_usd": "...",
    "min_billable_minutes": 1,
    "quoted_at": "2026-11-01T18:04:39.950Z",
    "expires_at": "2026-11-02T18:04:39.950Z"
  },
  "max_amount_usd": "250",
  "amounts": {
    "reserved_usd": "...",
    "settled_usd": "..."
  },
  "counts": {
    "queued": 40,
    "running": 4,
    "publishing": 0,
    "succeeded": 12,
    "needs_review": 0,
    "failed": 0,
    "skipped": 1,
    "cancelled": 0
  },
  "output": {
    "type": "hosted"
  },
  "webhook_url": "https://hooks.example.com/lumen",
  "metadata": {
    "project": "warehouse-q4"
  }
}

List batches

GET /batches?limit=20&cursor=...

Confirm or resume

POST /batches/{id}/confirm, no body. Returns the batch object.

Cancel

POST /batches/{id}/cancel, no body. Returns the batch object.

If your Lumen access ends, you can no longer create or confirm batches, but you can still list, read and cancel your existing batches and download their results. Batches still open 24 hours after access ended are cancelled as above: queued items are not charged, and items already running finish and are charged normally.

List items

GET /batches/{id}/items?status=succeeded&limit=100&cursor=... · limit is 1–500 (default 100); status filters by item status.

{
  "object": "list",
  "data": [
    {
      "object": "item",
      "id": "5a1e9a42-0b9e-4c55-8f0e-2a7b0f9c1d33",
      "batch_id": "8d0c7c1e-4b0f-4f1a-9a57-0f3f6a0f2b11",
      "position": 0,
      "input": {
        "type": "url",
        "index": 0,
        "host": "storage.example.com"
      },
      "status": "succeeded",
      "reason": null,
      "duration_s": 600,
      "container_duration_s": 600.5,
      "covered_s": 600,
      "billing": "charged",
      "billable_minutes": 10,
      "charged_usd": "...",
      "result_available": true,
      "result_expires_at": "2026-12-01T18:40:02.000Z",
      "created_at": "2026-11-01T18:02:11.204Z",
      "finished_at": "2026-11-01T18:40:02.000Z"
    }
  ],
  "next_cursor": null
}
Item fieldDescription
duration_sDuration of the video stream. This is what we label and quote.
container_duration_sDuration of the whole file, which can be longer when audio runs past the video.
covered_sVideo time labeled in the result.
billable_minutesCovered time, capped at the video duration and rounded up to whole minutes.
billingcharged, reused (the same content was already charged within the reuse window), not_billed, or pending while the item is not finished.
input{ "type": "url", "index", "host" } for URL inputs (the URL itself is never returned), or { "type": "source", "source", "key" } for bucket inputs.

Get a result

GET /items/{id}/result

{
  "object": "result",
  "item_id": "5a1e9a42-0b9e-4c55-8f0e-2a7b0f9c1d33",
  "schema": "lumen.egocentric-actions.v1",
  "sha256": "9b1f...",
  "bytes": 48213,
  "url": "https://...",
  "url_expires_at": "2026-11-01T18:55:02.000Z",
  "retained_until": "2026-12-01T18:40:02.000Z"
}

Schema

GET /schema returns id (lumen.egocentric-actions.v1), tasks (the available versions, with latest marked) and json_schema (the full result JSON Schema).

Bucket permissions

Your own storage (optional, set up with us). To read inputs from, or write results back to, your own storage, you provide an S3-compatible bucket you control (AWS S3, Cloudflare R2, or another S3-compatible store) and credentials with read access to the input prefix and read and write access to the output prefix. Credentials without the permissions below are refused when the source is registered. Without a bucket, use presigned URL inputs and hosted results.

The credentials you give us need:

ScopePermissions
Read prefix (inputs)s3:ListBucket, s3:GetObject
Write prefix (outputs)s3:PutObject, s3:GetObject, s3:ListBucket (we read _COMPLETE.json back before replacing it; without s3:ListBucket a missing object answers 403 instead of 404)
Write prefix, optionals3:DeleteObject (removes the registration check object)

When the bucket is registered, and whenever its credentials are replaced, we check them with a small test object at <write prefix>.lumen-selftest/<random>.json: read of a missing key, write, read back, and two conditional writes that must be refused. Credentials missing a required permission are not accepted. The test object is deleted afterwards; without s3:DeleteObject it stays (a few bytes) and can be removed at any time.

AWS IAM example (inputs under raw/, outputs under lumen-out/):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::<bucket>",
      "Condition": {
        "StringLike": {
          "s3:prefix": [
            "raw/*",
            "lumen-out/*"
          ]
        }
      }
    },
    {
      "Effect": "Allow",
      "Action": "s3:GetObject",
      "Resource": [
        "arn:aws:s3:::<bucket>/raw/*",
        "arn:aws:s3:::<bucket>/lumen-out/*"
      ]
    },
    {
      "Effect": "Allow",
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::<bucket>/lumen-out/*"
    },
    {
      "Effect": "Allow",
      "Action": "s3:DeleteObject",
      "Resource": "arn:aws:s3:::<bucket>/lumen-out/.lumen-selftest/*"
    }
  ]
}

Cloudflare R2: an R2 API token with Object Read & Write on the bucket covers all of the above (R2 tokens are scoped per bucket, not per prefix).

Statuses and reasons

Batch status

StatusMeaning
preflightFetching and inspecting inputs, computing the quote
awaiting_confirmationQuote ready; waiting for confirm
runningItems are being processed
blockedPaused; see blocked_reason
completedEvery item succeeded
completed_with_errorsSome items succeeded and some did not
failedNo item succeeded; see failure_reason
cancelledCancelled at your request

A finished batch can change from completed_with_errors or failed to completed (or completed_with_errors) when we approve an item that was in needs_review. A batch.completed webhook announces the change.

blocked_reason

ReasonWhat to do
insufficient_fundsAdd funds, then call confirm. Blocked batches also resume on their own once the balance covers them.
over_limitThe batch exceeds your credit limit. Add funds, or contact us.
credentials_invalidThe registered bucket's credentials no longer work. Send us new credentials; the batch resumes.
payment_reversedContact us.

failure_reason

no_processable_items, all_items_failed, too_many_items, batch_too_large, listing_too_large, source_unavailable, credentials_invalid, preflight_timeout (inputs could not be inspected within 24 hours), processing_error.

Item status

queued, running, publishing (result being verified), succeeded, needs_review, failed, skipped, cancelled.

Item reason

None of these is charged.

ReasonMeaning
too_largeFile over 10 GB
too_longOver 70 minutes, measured as the longer of the video stream and the file
unsupported_container · unsupported_codec · no_video_streamThe file is not a supported video
bad_timestampsBroken or implausible timestamps (for example, the video starts far into the file). Re-mux and resubmit.
unreadable · fetch_failed · input_expiredWe could not read the input, or its URL expired before processing
input_mismatchThe input changed while we were processing it
decode_error · pipeline_error · timeout · processing_errorProcessing did not complete
invalid_resultThe result failed our verification and was discarded
qa_failedThe result did not pass quality checks; it is in needs_review
exceeds_quoteThe result would have cost more than the confirmed quote; it was not charged
verification_unavailable · preflight_unavailableTemporary; processing was not completed in time
batch_rejected · cancelledThe batch was rejected or cancelled

Webhooks

Set webhook_url on a batch to receive signed POST requests. We issue your signing secret through a secure channel. To rotate the secret, ask us for a new one; the old secret keeps working for 24 hours.

Events

TypeWhen
batch.awaiting_confirmationThe quote is ready and the batch is waiting for your confirm. It is not sent when the batch is confirmed automatically.
batch.blockedThe batch was paused (reason gives the blocked_reason)
batch.completedThe batch finished with status completed or completed_with_errors. It is sent again, with previous_status, if an approval changes the outcome.
batch.failedThe batch finished with no succeeded item
batch.cancelledThe batch finished after a cancel
item.completedAn item reached succeeded, needs_review or failed, or was approved

Payload

{
  "event_id": "item.completed:5a1e9a42-0b9e-4c55-8f0e-2a7b0f9c1d33",
  "type": "item.completed",
  "batch_id": "8d0c7c1e-4b0f-4f1a-9a57-0f3f6a0f2b11",
  "item_id": "5a1e9a42-0b9e-4c55-8f0e-2a7b0f9c1d33",
  "status": "succeeded",
  "sequence": 7,
  "occurred_at": "2026-11-01T18:40:02.311Z"
}

Headers and signature

Content-Type: application/json
X-Lumen-Event-Id: item.completed:5a1e...
X-Lumen-Event-Type: item.completed
X-Lumen-Timestamp: 1793556002
X-Lumen-Signature: v1=5f0c...[,v1=...]

The signature is HMAC-SHA256(secret, "<X-Lumen-Timestamp>.<raw request body>"), hex-encoded with the prefix v1=. During a secret rotation the header carries several comma-separated v1= values; accept the request if any one matches. When verifying, reject timestamps more than 5 minutes from your clock, and verify against the raw body bytes, before any JSON parsing.

Verification: Node.js

import crypto from "node:crypto";
import express from "express";

const SECRET = process.env.LUMEN_WEBHOOK_SECRET;

// Returns true only for a fresh timestamp and a matching v1= signature.
// Malformed headers return false; they never throw.
function verifyLumenSignature(secret, ts, header, rawBody, nowSeconds = Date.now() / 1000) {
  if (typeof ts !== "string" || !/^[0-9]{1,12}$/.test(ts)) return false;
  if (Math.abs(nowSeconds - Number(ts)) > 300) return false;
  const expected = crypto
    .createHmac("sha256", secret)
    .update(Buffer.concat([Buffer.from(`${ts}.`), rawBody]))
    .digest(); // 32 bytes
  let ok = false;
  for (const token of String(header ?? "").split(",")) {
    const m = /^v1=([0-9a-fA-F]{64})$/.exec(token.trim());
    if (!m) continue; // skip malformed tokens
    ok = crypto.timingSafeEqual(Buffer.from(m[1], "hex"), expected) || ok;
  }
  return ok;
}

const app = express();
app.post("/lumen", express.raw({ type: "application/json" }), (req, res) => {
  const ok = verifyLumenSignature(SECRET, req.get("X-Lumen-Timestamp"), req.get("X-Lumen-Signature"), req.body);
  if (!ok) return res.sendStatus(400);
  const event = JSON.parse(req.body.toString("utf8"));
  // Deduplicate on event.event_id; order per batch_id by event.sequence.
  res.sendStatus(204);
});

Verification: Python

import hashlib, hmac, json, os, re, time
from flask import Flask, request, abort

SECRET = os.environ["LUMEN_WEBHOOK_SECRET"].encode()
app = Flask(__name__)

def verify_lumen_signature(secret, ts, header, raw_body, now=None):
    """True only for a fresh timestamp and a matching v1= signature.
    Malformed headers return False; they never raise."""
    if not isinstance(ts, str) or not re.fullmatch(r"[0-9]{1,12}", ts):
        return False
    if abs((time.time() if now is None else now) - int(ts)) > 300:
        return False
    expected = hmac.new(secret, ts.encode("ascii") + b"." + raw_body, hashlib.sha256).digest()
    ok = False
    for token in (header or "").split(","):
        m = re.fullmatch(r"v1=([0-9a-fA-F]{64})", token.strip())
        if not m:
            continue  # skip malformed tokens
        ok = hmac.compare_digest(bytes.fromhex(m.group(1)), expected) or ok
    return ok

@app.post("/lumen")
def lumen_webhook():
    body = request.get_data()  # raw bytes
    if not verify_lumen_signature(SECRET, request.headers.get("X-Lumen-Timestamp"),
                                  request.headers.get("X-Lumen-Signature"), body):
        abort(400)
    event = json.loads(body)
    # Deduplicate on event["event_id"]; order per batch_id by event["sequence"].
    return "", 204

Result format

Each succeeded item has one JSON document. It follows the JSON Schema lumen.egocentric-actions.v1, returned in full by GET /schema. In outline:

FieldDescription
inputsha256 of your file, duration_s (the video stream), video_start_s.
covered_secondsVideo time labeled.
activity_segmentsA gap-free timeline of activity segments, each with start_s, end_s, action, object, hands, description.
hands.left · hands.rightGap-free per-hand timelines, each segment with start_s, end_s, action, grasp, object.
qaThe quality checks the result passed, and metrics.labeled_share.

Limits

LimitValue
Videos per batch2000 in total, URLs and prefix listings combined (more fails the batch with too_many_items; a listing too large to read fails with listing_too_large)
Video hours per batch200
File size per video10 GB (10,000,000,000 bytes)
Video length per video70 minutes
Request body4 MB
metadata32 keys, 8 KB
Open batches per account20 (more returns 429 over_limit)
Request rate300 requests per minute per account
Quote validity24 hours
Hosted result retention30 days
Result download link15 minutes

Errors

Errors use HTTP status codes and a JSON body with a stable code, a human-readable message and a retryable flag.

{
  "error": {
    "code": "invalid_request",
    "message": "inputs must hold 1–2000 entries.",
    "retryable": false
  }
}

Retry only when retryable is true (or on 429 and 503), with backoff. Reuse the same Idempotency-Key when retrying POST /batches.

CodeStatusMeaning
unauthenticated401Missing or invalid API key
missing_scope403The key does not have Lumen enabled; create a new key with Lumen enabled
not_found404No such batch, item or source for this account
idempotency_conflict409The Idempotency-Key was used for a different request
not_ready409Preflight is still running
quote_expired409The quote expired; a new one follows
quote_changed409The quote changed while you confirmed; read the batch and confirm again
blocked409The batch is blocked for a reason only we can clear
invalid_state409The batch or item is not in a state that allows this
result_unavailable409The item has no result
result_expired409The hosted result is past retention
timeout408The request body was not received in time; retry
too_large413The request body is too large
invalid_request422The request is malformed; message says why
rate_limited429More than 300 requests in a minute
over_limit429Too many open batches
unavailable503Temporary; retry with backoff

Billing

Lumen is billed per minute of processed video, from the same prepaid balance as Trio-Spark. Add credits in the console (Billing); there is no subscription. Before any processing, every batch gets a quote, and the quoted amount is held on your balance when the batch is confirmed. You are never charged more than the confirmed quote. Only succeeded items are charged, on the video time covered, rounded up to whole minutes with a minimum of one minute per item. For volume or a higher credit limit, talk to us.

Ready to send your first batch?

Create an account, add credits in the console (Billing), and create an API key with Trio-Lumen checked.

Go to the platform