Authentication
Keys look like sm_live_… and are made on the dashboard, shown once, stored hashed. Send one on every call to /v1:
Authorization: Bearer sm_live_…A key has scopes (analyses:read, analyses:write), a rate limit per minute (60 by default; the X-RateLimit-* headers say where you stand) and an optional monthly spend cap. The base URL is https://api.smartmediaprotocol.com.
1 · Upload
Ask for an upload URL, then PUT the bytes to it. The URL is good for an hour.
curl -X POST https://api.smartmediaprotocol.com/v1/uploads -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"filename":"IMG_1731.MOV","bytes":61230411,"content_type":"video/quicktime"}'
# → { "upload_id": "up_…", "upload": { "url": "…", "method": "PUT", "headers": { … } }, "max_bytes": 314572800, "max_seconds": 600 }
curl -X PUT "$UPLOAD_URL" -H "Content-Type: video/quicktime" --data-binary @IMG_1731.MOVAccepted: videos (mp4, mov, m4v, webm, mkv), audio (mp3, m4a, wav, flac, aac, ogg, opus, aif, caf) and images (jpg, png, heic, webp, tif, gif, bmp, avif). The response states the two limits the server enforces. Instead of an upload you may pass a public https URL as url when creating the analysis.
2 · Analyse
Create the job with the upload and the sidecar type you declare. The API never guesses the type from the content; without one, a video is video/edit, an audio file audio/speech, an image image/scene. The answer is 202 with the job, and the estimate is held on your balance.
curl -X POST https://api.smartmediaprotocol.com/v1/analyses -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"upload_id":"up_…","type":"video/footage","webhook_url":"https://example.com/hook","idempotency_key":"IMG_1731-v1"}'
# → { "id": "an_…", "status": "queued", "type": "video/footage", "estimate_cents": 20, … }
curl https://api.smartmediaprotocol.com/v1/analyses/an_… -H "Authorization: Bearer $KEY" # queued → running → done | failed| field | meaning |
|---|---|
| upload_id | url | One of the two. A URL must be https and public; the worker fetches it. |
| type | video/footage, video/edit, audio/speech, audio/music, image/scene, image/graphic. |
| options.consolidate | Video only: one readable paragraph per shot from a second model pass. 5¢ per minute more. |
| options.place | Look up the recording place from the file's GPS (default true). |
| options.language | A spoken-language hint as a BCP 47 tag. |
| webhook_url | Where to POST analysis.completed or analysis.failed. |
| idempotency_key | Repeating a request with the same key returns the same job instead of a second charge; a different request with the same key is a 409. |
| source | The original, when the upload is a proxy of it. See below. |
A job can be cancelled while it is queued (DELETE /v1/analyses/:id, the hold is released); once done, the same call deletes the stored result.
Proxies and the original
The analysis only ever needs a small copy of a video: 1280 px on the long side, 30 fps, a few megabits per second. Uploads are capped at 300 MB and 1280 px, so for anything larger you send a proxy and describe the original in source. Everything given there replaces what the API reads from the upload, so the sidecar's source block, the recording time and place and the camera facts are the original's; the API never relies on metadata embedded in the proxy. generated.proxy records what was actually analysed, and the result cache is keyed on the declared sha256.
"source": { "name": "IMG_4115.MOV", "bytes": 358077119, "sha256": "…64 hex…", "quick_hash": "…64 hex…", "container": "mov",
"created_at": "2026-08-25T14:05:42+08:00",
"location": { "lat": -8.6423, "lon": 115.154, "altitude": 36 },
"camera": { "make": "Apple", "model": "iPhone 17 Pro Max" },
"video": { "codec": "hevc", "width": 3840, "height": 2160, "fps": 59.97, "bit_rate": 54734192, "rotation": 90, "hdr": true, "color_transfer": "arib-std-b67" },
"audio": { "codec": "aac", "channels": 2, "sample_rate": 48000, "bit_rate": 189321 } }Keep the camera's zone offset in created_at: it gives the local time and the time of day. The quick hash is the SHA-256 of the size in bytes as decimal ASCII, then the first MiB, then the last MiB of the original.
3 · The result
curl "https://api.smartmediaprotocol.com/v1/analyses/an_…/result?format=smartmedia" -H "Authorization: Bearer $KEY" -o IMG_1731.MOV.smartmedia
curl "https://api.smartmediaprotocol.com/v1/analyses/an_…/result?format=text" -H "Authorization: Bearer $KEY" # the readable twin
curl "https://api.smartmediaprotocol.com/v1/analyses/an_…/result?format=json" -H "Authorization: Bearer $KEY" # the full internal analysisWrite the smartmedia document next to the original as <name>.smartmedia. The text format is a plain-text rendering of the same document for a language model or a person. Results are kept for 90 days. Listing: GET /v1/analyses?limit=&status=.
Webhooks
With a webhook_url, the API POSTs analysis.completed or analysis.failed with the job, signed with the key's webhook secret in the Smartmedia-Signature header (HMAC-SHA256 of the timestamp and the body). Deliveries retry with backoff; the dashboard shows each attempt and lets you send a test event. The secret is on the key's page and can be rotated.
Estimates, prices and caps
curl "https://api.smartmediaprotocol.com/v1/estimate?type=video/footage&duration_s=125" -H "Authorization: Bearer $KEY"
curl "https://api.smartmediaprotocol.com/v1/estimate?type=image/scene" -H "Authorization: Bearer $KEY"
curl https://api.smartmediaprotocol.com/v1/me -H "Authorization: Bearer $KEY" # balance, the key, every price| type | price | minimum |
|---|---|---|
| video/footage, video/edit | 20¢ per minute | 20¢ |
| audio/speech | 6¢ per minute | 5¢ |
| audio/music | 2¢ per minute | 2¢ |
| image/scene, image/graphic | 3¢ per image | 3¢ |
Money is integer cents on a prepaid balance. The estimate is held when a job is accepted and settled to the actual charge, never more than the estimate, when it ends; a failed job releases its hold. A file already analysed by the same account as the same type is served from the cache for nothing. Monthly spend caps exist per key and per account; a job that would cross one is refused with 402.
Errors
One shape, always:
{ "error": { "code": "input_too_large", "message": "Files may be at most 314572800 bytes. Downscale or trim first.", "details": { "max_bytes": 314572800 }, "request_id": "req_…" } }| code | status | when |
|---|---|---|
| unauthorized | 401 | No key, or a revoked one. |
| forbidden | 403 | A missing scope, or a suspended account. |
| validation_failed | 400 | The body is wrong; details.problems names each problem. |
| upload_missing | 404 | No such upload, or nothing was PUT to it yet. |
| input_too_large | 413 / 422 | Over the byte limit, or over 1280 px for a video. Send a proxy. |
| input_too_long | 422 | Over ten minutes, or under half a second. |
| input_unsupported | 422 | Not a media file, or a type the file cannot be (audio as video). |
| insufficient_balance, spend_cap_reached | 402 | Buy credit on the dashboard, or raise the cap. |
| idempotency_conflict | 409 | The key was used for a different request. |
| rate_limited | 429 | Wait Retry-After seconds. |
Limits
- Ten minutes per file; split longer footage.
- 300 MB per upload, and 1280 px on the long side for a video: send a proxy and declare the original.
- Results are kept for 90 days. Uploads are deleted once the job has run.
- The result cache is per account: nothing is shared across accounts, even for identical files.
Reference
- OpenAPI reference (Swagger UI) and openapi.json: every route and field.
- The protocol: what the document contains, per type.
- The dashboard: keys, jobs, the ledger, credit, webhooks.