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.
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.
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
done3Search and export a clip
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.
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.mp4On-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.
{
"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.