Quickstart

Upload a video, build its speech index, find a citable transcript moment, and export the matching clip through the complete Cloud REST P0 flow.

1Get an API key

Start the device flow, open the returned Cerul approval URL, and approve the code while logged in. Exchange the approved grant for a short-lived token, then create a scoped Workspace key.

Terminalbash
export CERUL_QUICKSTART_RUN_ID="$(date +%s)-$RANDOM"

AUTH=$(curl https://api.cerul.ai/v1/oauth/device/authorize \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "cerul-cli",
    "scope": [
      "workspace:read",
      "api_credentials:write",
      "libraries:read",
      "libraries:write",
      "library-items:write",
      "assets:read",
      "assets:write",
      "jobs:read",
      "jobs:write",
      "artifacts:read",
      "artifacts:write"
    ]
  }')

echo "Open: $(printf '%s' "$AUTH" | jq -r '.verification_uri_complete')"
# Approve the code in Cerul, then continue:

TOKEN=$(curl https://api.cerul.ai/v1/oauth/token \
  -H "Content-Type: application/json" \
  -d "{
    \"grant_type\": \"urn:ietf:params:oauth:grant-type:device_code\",
    \"device_code\": \"$(printf '%s' "$AUTH" | jq -r '.device_code')\",
    \"client_id\": \"cerul-cli\"
  }")
export CERUL_ACCESS_TOKEN=$(printf '%s' "$TOKEN" | jq -r '.access_token')

PRINCIPAL=$(curl https://api.cerul.ai/v1/principal \
  -H "Authorization: Bearer $CERUL_ACCESS_TOKEN")
export CERUL_WORKSPACE_ID=$(printf '%s' "$PRINCIPAL" | jq -r '.data.workspace_id')

KEY=$(curl "https://api.cerul.ai/v1/workspaces/$CERUL_WORKSPACE_ID/api-credentials" \
  -H "Authorization: Bearer $CERUL_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: create-quickstart-key-$CERUL_QUICKSTART_RUN_ID" \
  -d '{
    "name": "Quickstart",
    "scopes": [
      "libraries:read",
      "libraries:write",
      "library-items:write",
      "assets:read",
      "assets:write",
      "jobs:read",
      "jobs:write",
      "artifacts:read",
      "artifacts:write"
    ]
  }')
export CERUL_API_KEY=$(printf '%s' "$KEY" | jq -r '.data.secret')

2Add a video

Create or choose a library, request a direct upload URL, PUT the exact reserved bytes with the returned headers, then authenticate the completion before indexing. Keep the returned asset_id for later requests.

Requestcreate → direct PUT → complete → index
FILE_SIZE=$(wc -c < ./talk.mp4 | tr -d ' ')

LIBRARY=$(curl https://api.cerul.ai/v1/libraries \
  -H "Authorization: Bearer $CERUL_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: create-quickstart-library-$CERUL_QUICKSTART_RUN_ID" \
  -d '{
    "name": "Quickstart",
    "ingestion_profile": {
      "id": "quickstart",
      "version": "1",
      "required_capabilities": [],
      "enrichment_schema": {},
      "segmentation_policy": {},
      "metadata_schema": {}
    }
  }')
export CERUL_LIBRARY_ID=$(printf '%s' "$LIBRARY" | jq -r '.data.id')

UPLOAD=$(curl https://api.cerul.ai/v1/uploads \
  -H "Authorization: Bearer $CERUL_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: upload-quickstart-$CERUL_QUICKSTART_RUN_ID" \
  -d "{
    \"filename\": \"talk.mp4\",
    \"media_type\": \"video/mp4\",
    \"byte_size\": $FILE_SIZE,
    \"execution_policy\": \"cloud_required\"
  }")

export CERUL_ASSET_ID=$(printf '%s' "$UPLOAD" | jq -r '.data.asset_id')
UPLOAD_ID=$(printf '%s' "$UPLOAD" | jq -r '.data.id')
UPLOAD_URL=$(printf '%s' "$UPLOAD" | jq -r '.data.upload_url')
UPLOAD_CONTENT_TYPE=$(printf '%s' "$UPLOAD" | jq -r '.data.required_headers["Content-Type"]')
UPLOAD_CONTENT_LENGTH=$(printf '%s' "$UPLOAD" | jq -r '.data.required_headers["Content-Length"]')
CONTENT_SHA256=$(shasum -a 256 ./talk.mp4 | awk '{print $1}')
DURATION_SECONDS=$(ffprobe -v error -show_entries format=duration -of csv=p=0 ./talk.mp4)
curl -X PUT "$UPLOAD_URL" \
  -H "Content-Type: $UPLOAD_CONTENT_TYPE" \
  -H "Content-Length: $UPLOAD_CONTENT_LENGTH" \
  --upload-file ./talk.mp4

curl -X POST "https://api.cerul.ai/v1/uploads/$UPLOAD_ID/complete" \
  -H "Authorization: Bearer $CERUL_API_KEY" \
  -H "Idempotency-Key: complete-quickstart-$CERUL_QUICKSTART_RUN_ID"

curl -X POST "https://api.cerul.ai/v1/libraries/$CERUL_LIBRARY_ID/items" \
  -H "Authorization: Bearer $CERUL_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: attach-quickstart-$CERUL_QUICKSTART_RUN_ID" \
  -d "{\"asset_id\": \"$CERUL_ASSET_ID\"}"

INDEX=$(curl https://api.cerul.ai/v1/index \
  -H "Authorization: Bearer $CERUL_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: prepare-quickstart-$CERUL_QUICKSTART_RUN_ID" \
  -d "{
    \"asset_id\": \"$CERUL_ASSET_ID\",
    \"library_id\": \"$CERUL_LIBRARY_ID\",
    \"duration_seconds\": $DURATION_SECONDS,
    \"content_sha256\": \"$CONTENT_SHA256\",
    \"index_level\": \"speech_only\",
    \"execution_policy\": \"cloud_required\"
  }")

JOB_ID=$(printf '%s' "$INDEX" | jq -r '.data.id')
while :; do
  STATUS=$(curl -s "https://api.cerul.ai/v1/jobs/$JOB_ID" \
    -H "Authorization: Bearer $CERUL_API_KEY" | jq -r '.data.status')
  [ "$STATUS" = "succeeded" ] && break
  [ "$STATUS" = "failed" ] && { echo "Preparation failed"; exit 1; }
  [ "$STATUS" = "canceled" ] && { echo "Preparation canceled"; exit 1; }
  sleep 5
done

Search the uploaded asset, take the transcript evidence time range, request an asynchronous clip export, poll it, then fetch both the artifact record and its authenticated content.

RequestPOST /v1/search → POST /v1/clips → artifact content
SEARCH=$(curl https://api.cerul.ai/v1/search \
  -H "Authorization: Bearer $CERUL_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{
    \"query\": \"when did they discuss scaling laws\",
    \"scope\": {
      \"library_ids\": [],
      \"asset_ids\": [\"$CERUL_ASSET_ID\"]
    },
    \"execution_policy\": \"cloud_required\"
  }")

export CERUL_EVIDENCE_ID=$(printf '%s' "$SEARCH" | jq -r '.data[0].evidence.id')
START_SECONDS=$(printf '%s' "$SEARCH" | jq -r '.data[0].evidence.start_seconds')
END_SECONDS=$(printf '%s' "$SEARCH" | jq -r '.data[0].evidence.end_seconds')

CLIP=$(curl https://api.cerul.ai/v1/clips \
  -H "Authorization: Bearer $CERUL_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: clip-quickstart-$CERUL_QUICKSTART_RUN_ID" \
  -d "{
    \"asset_id\": \"$CERUL_ASSET_ID\",
    \"start_seconds\": $START_SECONDS,
    \"end_seconds\": $END_SECONDS,
    \"evidence_ids\": [\"$CERUL_EVIDENCE_ID\"],
    \"execution_policy\": \"cloud_required\"
  }")

CLIP_JOB_ID=$(printf '%s' "$CLIP" | jq -r '.data.id')
while :; do
  CLIP_STATUS=$(curl -s "https://api.cerul.ai/v1/jobs/$CLIP_JOB_ID" \
    -H "Authorization: Bearer $CERUL_API_KEY" | jq -r '.data.status')
  [ "$CLIP_STATUS" = "succeeded" ] && break
  [ "$CLIP_STATUS" = "failed" ] && { echo "Clip export failed"; exit 1; }
  [ "$CLIP_STATUS" = "canceled" ] && { echo "Clip export canceled"; exit 1; }
  sleep 5
done

ARTIFACTS=$(curl "https://api.cerul.ai/v1/jobs/$CLIP_JOB_ID/artifacts" \
  -H "Authorization: Bearer $CERUL_API_KEY")
ARTIFACT_ID=$(printf '%s' "$ARTIFACTS" | jq -r '.data[0].id')

curl "https://api.cerul.ai/v1/artifacts/$ARTIFACT_ID" \
  -H "Authorization: Bearer $CERUL_API_KEY" | jq .

curl "https://api.cerul.ai/v1/artifacts/$ARTIFACT_ID/content" \
  -H "Authorization: Bearer $CERUL_API_KEY" \
  --output quickstart-clip.mp4

On-device search stays inside the Cerul Desktop app. This public reference documents the hosted Cloud API only.

4Read the response

P0 search results are transcript evidence ranked by score. Each one carries the exact time range your agent can cite or pass to the clip endpoint.

Illustrative response: the payload shape is contract-valid, while evidence text, timestamps, and scores depend on the video you upload.

Response200 OK · illustrative
{
  "request_id": "req_7c41ad",
  "execution": { "location": "cloud" },
  "usage": { "billable": false, "quantity": 0, "unit": "request" },
  "warnings": [],
  "data": [
    {
      "score": 0.83,
      "evidence": {
        "id": "ev_7c41ad",
        "asset_id": "asset_from_upload",
        "kind": "transcript",
        "start_seconds": 768,
        "end_seconds": 794,
        "quote": "When they discuss scaling laws, loss still matters more than benchmarks.",
        "locators": [{ "type": "cloud", "url": "https://api.cerul.ai/v1/artifacts/artifact_7c41ad" }]
      }
    }
  ]
}

What the evidence fields mean

Evidence is the unit of trust in Cerul. A score tells you how relevant a hit is; evidence tells you why, and lets a human or an agent verify the claim against the original video without rewatching it.

kindCloud REST P0 returns transcript evidence. Visual evidence is not part of this quickstart.
start_secondsThe second the moment begins. Seek any player here and you land on the quote.
end_secondsWhere the moment ends, so you can clip or replay exactly the span that supports it.
quoteThe matching transcript text. Verify it against the source before publishing a verbatim quotation.
locatorsLocal or cloud URLs that resolve the supporting evidence.
id · asset_idStable references to this piece of evidence and the video it lives in.

Next steps