CESOERA

Developers

Business API

Check images and videos for signs of AI generation from your own servers - for example, everything a user posts to your platform - without anyone opening the CESOERA website.

Getting access

API keys are issued on request to Business accounts. Contact us with your account email and expected volume. API calls use the same credits as scans on the website: one credit per image and, for video, 2-18 credits by length (measured by us, never taken from the request), drawn from your plan's monthly balance.

Authentication

Send your key in the Authorization header. Keys start with ces_live_. Keep them on your servers - never in a browser, mobile app or public repository. If a key leaks, ask us to revoke it and we will issue a new one.

Authorization: Bearer ces_live_XXXXXXXX_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Detect an image

POST /api/v1/detect/image accepts either an image URL (JSON) or the image itself (multipart). JPEG, PNG and WebP up to 15 MB. reference is optional - your own id for the item, returned with the result.

By URL

curl -X POST https://cesoera.com/api/v1/detect/image \
  -H "Authorization: Bearer $CESOERA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: post-8841" \
  -d '{"url": "https://images.example.com/uploads/8841.jpg", "reference": "post-8841"}'

By upload

curl -X POST https://cesoera.com/api/v1/detect/image \
  -H "Authorization: Bearer $CESOERA_API_KEY" \
  -F "file=@photo.jpg" \
  -F "reference=post-8841"

Node.js

const res = await fetch("https://cesoera.com/api/v1/detect/image", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.CESOERA_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": postId,
  },
  body: JSON.stringify({ url: imageUrl, reference: postId }),
});
if (res.status === 503 || res.status === 429) {
  const wait = Number(res.headers.get("Retry-After") ?? 10);
  // retry after `wait` seconds
}
const detection = await res.json();

Python

import os, requests

r = requests.post(
    "https://cesoera.com/api/v1/detect/image",
    headers={"Authorization": f"Bearer {os.environ['CESOERA_API_KEY']}",
             "Idempotency-Key": post_id},
    json={"url": image_url, "reference": post_id},
    timeout=60,
)
detection = r.json()

The response

{
  "id": "4f0c1c7e-9b0a-4d1e-9a51-2d7f3c1b8e20",
  "object": "detection",
  "created_at": "2026-09-14T18:21:04.512Z",
  "status": "completed",
  "content_type": "image",
  "input": "url",
  "reference": "post-8841",
  "verdict": "strong_indication",
  "ai_likelihood": 0.91,
  "confidence": "high",
  "model": "…",
  "credits_charged": 1,
  "processing_ms": 412,
  "error_code": null,
  "request_id": "…",
  "disclaimer": "CESOERA returns a probabilistic indicator, not proof. …"
}
verdictMeaning
strong_indicationStrong signs of AI generation.
some_indicationSome signs of AI generation.
inconclusiveNot enough evidence either way.
low_indicationLittle sign of AI generation.

ai_likelihood is on the same 0-1 scale as the website, aligned with the verdict bands (0-0.25 low, 0.25-0.5 inconclusive, 0.5-0.75 some, 0.75-1 strong). Results are probabilistic: use them to prioritise human review or label content, not as proof about a specific person.

Detect a video (asynchronous)

POST /api/v1/detect/video takes an MP4, MOV, WebM or M4V file (multipart file) or a direct video URL (JSON url), up to 100 MB and 10 minutes. Videos are analysed frame by frame, which takes from a few seconds to a few minutes, so the call returns 202 Accepted immediately with a detection in status queued. The credits shown are held, measured from the video's real duration; they settle to the actual cost on completion and are fully refunded on failure.

curl -X POST https://cesoera.com/api/v1/detect/video   -H "Authorization: Bearer $CESOERA_API_KEY"   -H "Content-Type: application/json"   -H "Idempotency-Key: video-5521"   -d '{"url": "https://media.example.com/v/5521.mp4",
       "reference": "video-5521",
       "callback_url": "https://api.example.com/cesoera/webhook"}'
HTTP/1.1 202 Accepted
Location: /api/v1/detections/0d6b…

{ "id": "0d6b…", "object": "detection", "status": "queued", "content_type": "video",
  "duration_seconds": 42.5, "credits_charged": 5, "verdict": null, "queue_position": 1, … }

Then either poll GET /api/v1/detections/{id} every few seconds until status is completed or failed, or supply callback_url (public https://) and we will POST the finished detection to you. Statuses: queuedprocessing completed | failed. Videos are processed one at a time; if the queue is full the call returns 503 with Retry-After and nothing is charged.

Webhooks

When a video with a callback_url finishes, we send POST with JSON { "type": "detection.completed" | "detection.failed", "data": { …detection… } }. Answer with any 2xx within 10 seconds. We retry twice (after 5 and 30 seconds), and webhook_status on the detection shows whether delivery succeeded. Polling always works as a fallback.

Every delivery carries CESOERA-Signature: t=<unix time>,v1=<hex>: an HMAC-SHA256 of <t>.<raw body> using, as the secret, the SHA-256 hex digest of your API key. Verify it on the raw body before trusting the payload, and reject timestamps older than 5 minutes.

import crypto from "node:crypto";

const secret = crypto.createHash("sha256").update(process.env.CESOERA_API_KEY).digest("hex");

function verify(rawBody, header) {
  const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(v1, "hex"));
}

Fetch a result again

curl https://cesoera.com/api/v1/detections/4f0c1c7e-9b0a-4d1e-9a51-2d7f3c1b8e20 \
  -H "Authorization: Bearer $CESOERA_API_KEY"

Sending the same Idempotency-Key again returns the stored result with the header Idempotent-Replayed: true, without running the model or charging again. Use your own item id as the key so retries after timeouts are always safe.

Usage and credits

curl https://cesoera.com/api/v1/usage -H "Authorization: Bearer $CESOERA_API_KEY"

Returns your plan, credits used and remaining this calendar month (UTC), and the key's rate limit. Checking usage is free. A failed detection is never charged.

Errors and retries

Errors share one shape:

{ "error": { "code": "rate_limit_reached", "message": "…", "requestId": "…", "details": {} } }
StatuscodeWhat to do
400validation_failed, file_required, unsupported_content_typeFix the request.
400invalid_url, unsafe_url, url_unreachable, url_emptyThe URL must be a reachable, public image address.
401invalid_api_keyCheck the key; it may have been revoked.
413 / 415file_too_large, url_too_large, unsupported_type, invalid_file_signatureImages: JPEG, PNG or WebP up to 15 MB. Videos: MP4, MOV, WebM or M4V up to 100 MB.
413 / 422video_too_long, video_unreadableVideos must be readable and 10 minutes or shorter.
400invalid_callback_urlcallback_url must be a public https:// address.
429rate_limit_reachedRetry after the Retry-After header (seconds).
429scan_limit_reachedMonthly credits are used up; do not retry until they renew.
502detection_failedRetry later with the same Idempotency-Key. Not charged.
503capacity_exhaustedThe service is busy. Retry after Retry-After.

Limits and privacy