raidxai
Official Python SDK for the Raid AI detection API — detect AI-generated and manipulated media (images, audio, video), analyze documents for tampering and AI generation, and fact-check media against the public record.
Sync and async clients, Python 3.10+. Full API reference: https://docs.raidxai.com.
Install
pip install raidxai
Authentication
Every request uses a developer token. Create one in the Raid AI dashboard (Settings → API keys) and keep it server-side — never ship it in client code.
from raidxai import RaidClient, FileInput
raid = RaidClient(
api_key="<your-api-key>", # or os.environ["RAID_API_KEY"]
# base_url="https://api.raidxai.com", # optional — defaults to the production API
# timeout=60.0,
# max_retries=2,
# auth_header="bearer", # or "x-api-key"
)
Each API key carries scopes (image, audio, video, fact-check) — a call to a modality
your key isn't scoped for raises RaidApiError with a 403 (api_key.scope_missing).
Usage
Images (synchronous)
res = raid.images.process(FileInput.from_path("suspect.jpg"))
print(res.images[0].verdict, res.images[0].confidence)
# …or from a URL:
raid.images.process_from_url("https://example.com/photo.jpg")
Audio (synchronous)
from raidxai import VoiceWorkflow
res = raid.audio.process(
FileInput.from_path("clip.mp3"),
workflow_type=VoiceWorkflow.AI_DETECTION_ONLY,
)
print(res.is_ai_detected, res.detection_confidence)
Any file with a decodable audio track is accepted — WAV, MP3, FLAC, M4A/AAC, OGG/Opus, WebM, AMR,
WMA and more, including the soundtrack of an MP4 or WebM video. Acceptance is decided by the file's
content, not its name or content type, and the audio is normalized server-side before analysis.
A file with no audio track fails with VOICE_ANALYSIS:UNSUPPORTED_FORMAT; AI detection also needs
at least 2 seconds of audio and refuses clips over the account's limit (35 minutes by default) with
VOICE_ANALYSIS:CLIP_TOO_SHORT / VOICE_ANALYSIS:CLIP_TOO_LONG — all before any credit is spent.
detection_confidence is confidence in the verdict that was reported, measured as distance
past the decision boundary — not the probability that the audio is AI-generated. 0.5 means the
clip landed on the boundary, and the opposite verdict's confidence is not
1 - detection_confidence. Rank and shade by it; don't show it to end users as a percentage
likelihood.
metadata carries the detector's own detail, typed as VoiceDetectionDetails (every field
optional, keyed as the detector reports it):
d = res.metadata
print(d.provider) # which detector produced the verdict — an open string, don't hard-code it
print(d.score) # 0–1 percentile vs the detector's own genuine speech: the figure to show
print(d.score_basis) # "percentile_vs_genuine" — only read `score` as a percentile when it says so
print(len(d.chunks or [])) # per-window timeline, when the detector sends one
print(d.model_extra) # keys the spec leaves untyped ride through here
p_fake is a raw ranking, not a probability (probability_calibrated is false), so never show
it as a percentage. Which detector runs is chosen per account by the Raid AI platform; the SDK
exposes no selector. The response's workflow_type is the workflow's name (for example
"AiDetectionOnly") even though the request takes the number.
Video (asynchronous — submit then poll)
# One call: submit and wait for the terminal verdict.
job = raid.video.submit_and_wait(
FileInput.from_path("clip.mp4"),
client_duration_seconds=42,
interval_seconds=3,
timeout_seconds=300,
)
print(job.status, job.result.verdict if job.result else None)
# …or drive it yourself:
submitted = raid.video.submit(FileInput.from_path("clip.mp4"), client_duration_seconds=42)
state = raid.video.get_job(submitted.job_id)
Documents (asynchronous — analyze then poll)
One upload returns both a document-level forensics verdict and per-page tampering /
AI-generation results. A full analysis runs for minutes, so analyze hands back the record
(usually still processing) rather than a finished verdict.
analysis = raid.documents.analyze_and_wait(
FileInput.from_path("statement.pdf"),
doc_type="bank_statement",
interval_seconds=5,
timeout_seconds=900,
)
print(analysis.fused_verdict, analysis.fused_probability) # "reject", 0.94
for page in analysis.pages:
print(page.page_number, page.verdict, len(page.edited_regions))
# …or drive it yourself and persist the id:
started = raid.documents.analyze(FileInput.from_path("invoice.pdf"), doc_type="invoice_po")
state = raid.documents.get(str(started.id))
Accepted formats are PDF, PNG, JPEG and WebP. An image is a single page and has no PDF structure, so its forensics verdict carries the consistency lane alone.
Four things will bite you if you assume otherwise:
- Render
page.verdict, notpage.localized_editing. A wholly AI-generated page reportslocalized_editing=False— nothing on it was edited. Onlyverdictfolds in the AI check. - Two verdict vocabularies. The document decides with
fused_verdict(accept/review/reject/insufficient_quality); a page describes itself withverdict(authentic/edited/ai_generated). - Two coordinate spaces in one response. Page regions (
edited_regions) are original-image pixels againstpage_width/page_height; forensics findingbboxvalues are normalized[x0, y0, x1, y1]in 0–1. Divide the pixels by the page size to draw both on one overlay. - Never re-upload to retry. A still-
processinganalysis already exists, and a second upload is a second charge. Pollgetwith the id you already have.
Fetch the rasters to draw on:
image = raid.documents.page_image(str(analysis.id), 2) # .data bytes + .content_type
crop = raid.documents.evidence(str(analysis.id), feature="tampering", page=2)
credit_info() returns the live balance and upload limits (size ceilings, batch limit, recommended
concurrency) — read those rather than hard-coding the defaults; they are server-side settings.
Fact-checking (asynchronous)
job = raid.fact_checking.submit_and_wait(
FileInput.from_path("photo.jpg"),
"image",
user_context="Claimed to be from the 2024 election.",
)
print(job.result.summary if job.result else None)
Image batch mode (queue now, collect later)
Queue images for analysis in a later scheduled window, at a reduced credit cost. submit_batch
returns a batch ID immediately — no verdicts. Batch mode is images-only.
batch = client.images.submit_batch([
FileInput.from_path("a.jpg"),
FileInput.from_path("b.jpg"),
])
print(batch.batch_id, batch.credits_charged, batch.credits_per_item)
# Later — poll `is_terminal`, not a status allow-list.
status = client.images.get_batch(str(batch.batch_id))
if status.is_terminal:
page = client.images.batch_results(str(batch.batch_id))
for item in page.items or []:
print(item.ordinal, item.file_name, item.result.verdict if item.result else None)
Two differences from process are structural, not temporary:
- Turnaround is up to a full processing window — hours, not seconds. Never put a batch call where a person is waiting.
- Results are basic tier.
deep_analysis,generatorsandheatmap_urlare alwaysNone, whatever your plan. Those are realtime-only.
Credits are deducted when the batch is accepted and refunded for any image that ultimately fails.
cancel_batch refunds the images still waiting; ones already being analyzed are left to finish.
Match results to your inputs with ordinal — the image's position in the original submission.
File names are echoed back but are not guaranteed unique.
submit_batch_and_wait exists but is rarely what you want: it holds the process open for the whole
window. Prefer submitting, persisting the ID, and collecting later.
Credits and limits
Every modality exposes a credit_info() read: the current balance plus that modality's live limits —
per-call cost, max file size, allowed formats, and the batch or daily caps where they apply. The
shapes differ because the pricing differs (images are per image, video per analyzed frame, audio per
workflow), so there is one call per modality rather than one shared endpoint. Audio is the one
modality with no format list: allowed_file_formats is always "*" (kept for older clients) and
accepts_any_audio is True.
image = raid.images.credit_info()
print(image.current_balance, image.credit_cost_per_image, image.max_file_size_mb)
raid.audio.credit_info()
raid.video.credit_info()
raid.documents.credit_info()
raid.fact_checking.credit_info()
Read these rather than hard-coding the defaults — they are account settings and can change without an SDK release. Any valid API token may call them; no scope is required.
Async
Every resource has an async twin on AsyncRaidClient with the same method names:
import asyncio
from raidxai import AsyncRaidClient, FileInput
async def main():
async with AsyncRaidClient(api_key="<your-api-key>") as raid:
res = await raid.images.process(FileInput.from_path("photo.jpg"))
print(res.images[0].verdict)
asyncio.run(main())
Errors
Non-2xx responses raise RaidApiError (.status, .code, .message, .body, plus .is_auth /
.is_payment_required / .is_rate_limited). Transient 429/5xx responses are retried automatically
with exponential backoff (max_retries). A submit_and_wait that never finishes raises RaidTimeoutError.
from raidxai import RaidApiError, RaidTimeoutError
try:
raid.images.process(file)
except RaidApiError as err:
if err.is_auth:
print("bad or unscoped token:", err.code)
elif err.is_payment_required:
print("out of credits:", err.code)
else:
print(err.status, err.code, err.message)
except RaidTimeoutError as err:
print("job did not finish in time; last status:", err.last_status)
Types
Response models are Pydantic v2 classes generated from the API's OpenAPI spec (ImageForensicsResponse,
VideoJob, Verdict, JobStatus, …), so attributes are snake_case and tracked against the server contract.
Development
uv venv --python 3.12 && source .venv/bin/activate
uv pip install -e ".[dev]"
./scripts/generate.sh # regenerate src/raidxai/_generated/models.py from ../spec/openapi.yaml
ruff check .
pytest
Run the live smoke test against a real tier (auto-skips without the key):
RAID_API_KEY=<your-api-key> RAID_API_BASE_URL=<raid-ai-api-url> pytest tests/test_live_smoke.py
Metadata
Release files for raidxai 0.2.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| raidxai-0.2.1.tar.gz | 34.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| raidxai-0.2.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 77.2 kB
Release files / raidxai-0.2.1.tar.gz
| Download URL | raidxai-0.2.1.tar.gz |
|---|---|
| Size | 34.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a383629a3b99a63c0a90654d45d7571ee82bf16daebd7cf75d013b3dfb85014d
|
|
BLAKE2b-256 checksum How to use checksums |
9671d8ed4e2ac7ee990e101adb5ce551e86dc173db41e1a77791258030c939aa
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 21, 2026.
Transparency logRelease files / raidxai-0.2.1-py3-none-any.whl
| Download URL | raidxai-0.2.1-py3-none-any.whl |
|---|---|
| Size | 43.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f87d51589d7ab443e597c913376d818275128b2ba30957a504c248f9d6f8b971
|
|
BLAKE2b-256 checksum How to use checksums |
741c83a86c10cba73bfc21eb4b522ab0680a3c95b05bd009d0752ef49b310e40
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 21, 2026.
Transparency log