Your whole scoring engine, over plain REST.
Submit creatives, poll for results, pipe scores into your own reporting, then read the agent's decisions or hand them over. JSON in, JSON out, from a token to a webhook.
Authentication
Every request is authenticated one of two ways. For a server or a scheduled pipeline, mint an API key and send it in the x-api-key header: keys are long-lived, scoped (read or read + write), optionally expiring, and revocable. For a short session, exchange email and password for a bearer token that lasts 60 minutes; call POST /api/auth/refresh with a still-valid token to roll it over before it lapses. API keys are on the Enterprise plan.
# Recommended for servers: mint a long-lived API key (Enterprise plan)
POST /api/keys
{"name": "reporting-pipeline", "scopes": ["read", "write"], "expires_in_days": 90}
# Response — the key is shown ONCE, store it now
{"key": "azk_live_...", "scopes": ["read", "write"], "expires_at": "2026-10-11T00:00:00Z"}
# Send it on every request
x-api-key: azk_live_...
# Alternatively, exchange email + password for a short-lived bearer token
POST /api/auth/login
{"email": "you@example.com", "password": "..."}
# Response — the token expires 60 minutes after issue
{"access_token": "eyJ...", "token_type": "bearer"}
# Use it the standard way
Authorization: Bearer <your_token>Mint an API key. The key value is returned once and never stored — save it immediately.
List your active keys (label, scopes, expiry, prefix — never the key value).
Revoke a key immediately.
Exchange credentials for a 60-minute bearer token.
Exchange a valid token for a fresh 60-minute token.
Returns your user profile and current plan.
Submit an analysis
Upload a video or audio file to start an analysis. The file is processed asynchronously: you'll get a job_id back immediately.
POST /api/jobs
Content-Type: multipart/form-data| Parameter | Required | Description |
|---|---|---|
| files | required | Video or audio file(s): mp4, mov, mp3, wav, m4a (max 100MB each) |
| batch_label | optional | A name for this ad or batch, e.g. "Summer 2026 hero" |
| platform | optional | Target platform: meta, tiktok, youtube |
| industry | optional | Your vertical, used for benchmarking |
// Response
{
"status": "queued",
"job_ids": ["3f9a2b1c-..."],
"batch_id": null
}You can also submit a YouTube URL instead of uploading a file:
POST /api/jobs/url
Content-Type: application/json
{"url": "https://youtube.com/watch?v=...", "label": "Summer hero v2"}Poll for results
Analysis typically completes in under a minute; allow up to ~2 minutes for large files. Poll the status endpoint until status reaches finished or failed.
Returns current status and progress (0 to 100)
Returns full analysis result when status is finished
// Status response
{
"job_id": "3f9a2b1c-...",
"status": "started", // queued | started | finished | failed
"progress": 62,
"stage": "acoustic_analysis"
}The result object
When status = "finished", the full analysis result is available at GET /api/jobs/{job_id}. Key fields:
{
"id": "3f9a2b1c-4d5e-6f7a-8b9c-0d1e2f3a4b5c",
"status": "finished",
"filename": "summer-hero-v2.mp4",
"result": {
"adzhi_score": {
"score": 74,
"percentile": 67,
"label": "Strong"
},
"hook_analysis": {
"score": 7.1,
"hook_type": "problem_statement",
"hook_decay_rate": 0.18
},
"acoustics": {
"voice_iq": 61,
"pacing": "moderate",
"energy_profile": [0.72, 0.68, 0.81, 0.95]
},
"persuasion_curve": {
"cta_intensity": 71,
"cta_momentum": 0.83
},
"emotional_trajectory": {
"emotion_shape": "rising",
"arc_score": 5.8
},
"act_structure": {
"structure_score": 68,
"structure_type": "hook_build_close",
"missing_elements": []
},
"top_fix": {
"signal": "hook",
"finding": "No voice in the first 1.5s weakens hook capture.",
"recommendation": "Re-cut to open on the voiceover."
},
"benchmarks": {
"hook_score": {"value": 7.1, "percentile": 68, "fleet_avg": 5.8},
"voice_iq": {"value": 61, "percentile": 43, "fleet_avg": 62}
},
"creative_tone": "urgent",
"wpm": 148,
"duration_sec": 32.4
},
"created_at": "2026-05-30T09:14:22Z",
"updated_at": "2026-05-30T09:16:05Z"
}List your analyses
Retrieve a paginated list of all your submitted analyses. The path is literally /api/jobs/all — all is the collection, not a placeholder. List responses return a summary projection: the full result blob is only returned on the single-job endpoint.
GET /api/jobs/all?limit=50&offset=0
// Optional filters
?status=finished // queued | started | finished | failed
?search=summer // searches filename
?sort=score_high // newest | oldest | score_high | score_low
// Response
{
"count": 142,
"jobs": [
{
"id": "3f9a2b1c-...",
"status": "finished",
"filename": "summer-hero-v2.mp4",
"result": { "adzhi_score": {"score": 74} /* + summary fields */ },
"created_at": "2026-05-30T09:14:22Z"
}
]
}Log a performance outcome
After your ad runs, record actual performance against the analysis. This builds your account's score-to-performance correlation model over time.
PUT /api/jobs/{job_id}/outcome
Content-Type: application/json
{
// roas: a multiplier · cvr: conversion rate as a percentage
"roas": 3.3,
"cvr": 4.1,
"notes": "revised hook, ran Meta only"
}
// Response
{"status": "ok", "roas": 3.3, "cvr": 4.1}Analyse a script, or a live ad
Two reads that don't need a fresh upload. /script scores raw text before anything is filmed; /inflight turns an existing analysis (or a script) plus live metrics into a Scale / Fix / Scrap call.
Hook, structure, rewrites and a brief from raw text. Instant, no queue.
POST /api/insights/script
Content-Type: application/json
{
"script": "Tired of dull skin? Here's the 60-second glow ritual...",
"context": "DTC skincare, women 25-40" // optional
}
// Response - instant, no queue
{
"hook_analysis": { "score": 7.4, "hook_type": "problem_statement" },
"act_structure": { "structure_score": 71, "missing_elements": ["proof"] },
"rewrites": { "variants": [ /* ... */ ] },
"brief": { "one_liner": "...", "hook": "...", "cta": "..." },
"audience": { "segments": [ /* ... */ ], "objections": [ /* ... */ ] },
"note": "Text-only - upload audio for acoustic Voice IQ + delivery scoring."
}A Scale / Fix / Scrap verdict for a running ad: the creative signals read beside the live numbers.
POST /api/insights/inflight
Content-Type: application/json
{
"job_id": "3f9a2b1c-...", // or "script": "..." for text-only
"roas": 1.9, "ctr": 0.7,
"spend": 840, "days_running": 6,
"target_roas": 3.0
}
// Response - a Scale / Fix / Scrap call on a running ad
{
"source": "job", // job | script
"recommendation": { /* scale | fix | scrap, with the rationale + evidence */ }
}The decision layer
Read the agent's open decisions, record a verdict, and set who decides per scope. Recording a verdict shapes the agent's per-kind learning; it never changes an ad. Delegation sets who decides, not whether spend moves.
The highest-leverage open decisions, severity-ranked, each with its evidence and honest confidence.
GET /api/insights/action-queue?limit=20
// Response - the highest-leverage OPEN decisions, severity-ranked.
// Resolved (accepted / rejected / modified) cards drop out.
{
"queue": [
{
"id": "d_8f2a...",
"job_id": "3f9a2b1c-...",
"action": "fix", // scale | fix | retire | ...
"severity": "high",
"confidence": 0.78,
"confidence_label": "calibrated to your account",
"trigger": "CTA delivered at 68% of your average energy",
"expected_lift": { "metric": "CTR", "band": "+8-14%", "validated": false },
"rollback": "Revert to the prior cut",
"status": "open"
}
],
"stats": { "open": 6, "accepted": 12 },
"learning": { /* per-kind acceptance shaping the order */ }
}Accept, reject or modify a decision. Records the verdict and tunes ranking. It does not touch the ad.
POST /api/insights/action-queue/disposition
Content-Type: application/json
{
"job_id": "3f9a2b1c-...",
"decision_id": "d_8f2a...",
// disposition: accepted | rejected | modified | feedback
"disposition": "accepted",
// kind is optional - feeds the agent's per-kind learning
"kind": "cta"
}
// Response - records your verdict + tunes ranking. It does NOT change the ad.
{"status": "ok", "disposition": "accepted", "decision_id": "d_8f2a...", "job_id": "3f9a2b1c-..."}Read or set who decides. Agent or human, for an account, campaign, ad set or ad.
# Who decides, per scope
GET /api/insights/autonomy
{
"settings": [
{ "scope_level": "campaign", "scope_id": "c_123", "scope_name": "Spring Sale", "mode": "agent" }
],
"account_mode": "human",
"delegated": 1,
// execution_live: decided + emailed, never spent (see note)
"execution_live": false
}
# Delegate a scope to the agent, or take it back
PUT /api/insights/autonomy
{
// scope_level: account | campaign | adset | ad
"scope_level": "campaign",
"scope_id": "c_123",
// mode: agent | human · scope_name is optional
"mode": "agent",
"scope_name": "Spring Sale"
}
{"status": "ok", "mode": "agent", "execution_live": false}execution_live is false by default. Delegating a scope to the agent records its decision and emails the account owner what it chose and why. It does not move spend or change the ad. Live execution against your ad account is a separate, credentialed rollout.
Webhooks
Instead of polling, register a webhook and AdZhi POSTs to your endpoint the moment a job reaches a terminal state. Configure it once from your account or via the API, then verify each delivery with the signing secret.
Set your webhook URL and mint (or rotate) the signing secret. The secret is returned once.
Read the current webhook URL and whether a secret is set.
Fire a signed test ping to confirm your endpoint is reachable.
# Set your webhook URL (and mint a signing secret)
PUT /api/webhook
Content-Type: application/json
{"url": "https://your-app.com/hooks/adzhi", "rotate_secret": true}
// Response - the secret is shown ONCE, store it now
{
"url": "https://your-app.com/hooks/adzhi",
"secret": "a1b2c3...",
"message": "Webhook configured. Store your secret - it won't be shown again."
}
# Send a test ping
POST /api/webhook/testEach delivery is a signed POST. Verify the X-AdZhi-Signature header (HMAC-SHA256 of the raw body, keyed with your secret) before trusting the payload. On failure, AdZhi makes up to 3 delivery attempts in total — the initial send plus two retries, spaced a few seconds apart.
POST https://your-app.com/hooks/adzhi
X-AdZhi-Event: job.completed // job.completed | job.failed
X-AdZhi-Delivery: 3f9a2b1c-... // the job id
X-AdZhi-Timestamp: 1717060565
X-AdZhi-Signature: sha256=<hmac> // HMAC-SHA256 of the raw body, keyed with your secret
{
"event": "job.completed",
"job_id": "3f9a2b1c-...",
"timestamp": 1717060565,
"data": { /* the full analysis result */ }
}
// Verify in your handler (Python):
// expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
// hmac.compare_digest(expected, signature.removeprefix("sha256="))Rate limits & errors
API access is on the Enterprise plan; every other plan uses the dashboard. While the API is in beta we run a fair-use policy rather than published per-request quotas. Requests are throttled per client on a sliding window, with tighter windows on the compute-heavy endpoints — uploads and the Claude-backed /insights/* reads — than on reads. When you cross a window you get a 429 Too Many Requests with a Retry-After header (and a retry_after_seconds field in the body); wait that long and retry.
// 429 Too Many Requests
Retry-After: 42
{
"detail": "Rate limit exceeded. Please slow down.",
"retry_after_seconds": 42
}Firm per-plan limits land at general availability. If you need a specific sustained throughput now, tell us the volume and we'll set it on your account.
Status codes
Errors return a JSON body with a detail string. These are the statuses you'll actually see:
| Status | Meaning | What it tells you |
|---|---|---|
| 200 | OK | The request succeeded. |
| 400 | Bad Request | The request was malformed — a missing field, or a value the endpoint could not accept. |
| 401 | Unauthorized | Missing, invalid or expired token or API key. Mint a fresh one and retry. |
| 403 | Forbidden | Authenticated, but your plan or key scope doesn't allow this. API access is an Enterprise capability. |
| 404 | Not Found | No such job or resource, or it is not owned by your account. |
| 409 | Conflict | The request conflicts with current state — e.g. an outcome already logged for that job. |
| 413 | Payload Too Large | The uploaded file exceeds the 100 MB limit. |
| 422 | Unprocessable Entity | Validation failed. The body parsed, but a field was out of range or the wrong type. |
| 429 | Too Many Requests | Rate limit hit. Back off and read the Retry-After header (seconds). |
| 5xx | Server Error | A 500, 502 or 503 from us or a dependency. Transient — retry with backoff; if it persists, contact support. |
Code examples
Python
import requests
import time
API_URL = "https://adzhi.co.uk"
TOKEN = "your_token_here"
headers = {"Authorization": f"Bearer {TOKEN}"}
# Submit an ad for analysis
with open("my_ad.mp4", "rb") as f:
resp = requests.post(
f"{API_URL}/api/jobs",
headers=headers,
files={"files": ("my_ad.mp4", f, "video/mp4")},
data={"batch_label": "Summer hero v2"},
)
resp.raise_for_status()
job_id = resp.json()["job_ids"][0]
print(f"Queued: {job_id}")
# Poll until complete
while True:
status = requests.get(
f"{API_URL}/api/jobs/status/{job_id}", headers=headers
).json()
print(f" {status['status']} - {status.get('progress', 0)}%")
if status["status"] in ("finished", "failed"):
break
time.sleep(5)
# Fetch full result
result = requests.get(
f"{API_URL}/api/jobs/{job_id}", headers=headers
).json()
score = result["result"]["adzhi_score"]["score"]
fix = result["result"]["top_fix"]["recommendation"]
print(f"AdZhi score: {score}/100")
print(f"Top fix: {fix}")curl
# Submit an ad
curl -X POST https://adzhi.co.uk/api/jobs \
-H "Authorization: Bearer $ADZHI_TOKEN" \
-F "files=@my_ad.mp4" \
-F "batch_label=Summer hero v2"
# Poll status
curl https://adzhi.co.uk/api/jobs/status/$JOB_ID \
-H "Authorization: Bearer $ADZHI_TOKEN"
# Fetch full result
curl https://adzhi.co.uk/api/jobs/$JOB_ID \
-H "Authorization: Bearer $ADZHI_TOKEN"
# Log an outcome
curl -X PUT https://adzhi.co.uk/api/jobs/$JOB_ID/outcome \
-H "Authorization: Bearer $ADZHI_TOKEN" \
-H "Content-Type: application/json" \
-d '{"roas": 3.3, "cvr": 4.1, "notes": "revised hook"}'Get API access
API access is included with Enterprise plans. Contact us to discuss volumes and rate limits.