MILLENNIUMS.AI documentation
Point MILLENNIUMS.AI at your AI application and it hacks it for you — in a private, throwaway sandbox — then hands back the vulnerabilities it proved, with a working exploit and a plain fix for each one.
You don't need a security team or a single line of attack code. Give it a target (a URL, a repository, or an API), and autonomous agents probe the AI attack surface the way a real attacker would: prompt injection, tool and agent abuse, data leakage, RAG flaws, runaway cost, and the rest of the OWASP LLM Top 10. Every finding you see is one it actually pulled off, with the exact steps to reproduce it.
This site has three parts. Core concepts explains what the tool does and why. Guides walk you through real tasks. The API reference documents every endpoint so you can drive scans from CI or your own scripts.
Quickstart — in the browser
The fastest way to see a result. No install, no card.
- Create your account. Go to scan.millenniums.ai/app and sign up with your email. A work email is best; a personal one works too. You get a free scan to start.
- Verify your email. Click the link we send you. This unlocks scanning. (Didn't arrive? Use "Resend" in the app.)
- Point it at a target. Paste your app's staging URL — the live address where it's running (not production). Optionally drag in your code (a
.zip) for a deeper scan. Press Start a scan. - Watch it work. The scan runs in an isolated sandbox and typically finishes in minutes. You'll see it probe each category live.
- Read the findings. Each proven vulnerability comes with its severity, impact, and a plain remediation. On a paid plan, every finding also includes a working proof of concept and a draft fix.
Quickstart — with the API
Prefer to drive it from a terminal or CI? Everything the app does is a REST call. Your access token is created at signup and shown in the app; it goes in an Authorization: Bearer header.
1. Check your account
Confirm your token works and see your plan and remaining quota.
curl https://scan.millenniums.ai/api/me \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
{ "tenant": "acme-3f9a2c", "plan": "starter", "poc": true,
"scans_used": 1, "scans_limit": 5, "verified": true }
2. Start a scan
Give it a target. Optionally add source (a repo or path) for a much deeper white-box scan, and a budget spend cap.
curl -X POST https://scan.millenniums.ai/api/scan \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"target":"https://staging.yourapp.com/chat","budget":10}'
{ "run_id": "20260731-abc123" }
3. Read the results
Poll the run until running is false, then read its findings.
curl https://scan.millenniums.ai/api/runs/20260731-abc123 \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
That's the whole loop: me → scan → runs/<id>. The API reference documents every field.
How it works
Five stages, all automatic:
- Point. You register a target: a staging URL, a repository, or an API endpoint.
- Attack. Autonomous agents spin up in an isolated Docker sandbox and probe the AI attack surface — no scripts for you to write.
- Prove. When an agent finds a weakness, it tries to exploit it for real. Only exploits that actually fire become findings; unproven leads are held separately.
- Fix. Each finding comes with a plain remediation and, when you want it, a draft pull request your engineers review.
- Gate. On the CI plans, a scoped scan runs on each pull request and blocks the merge on a proven, net-new vulnerability.
When the scan ends, the sandbox — and everything in it — is destroyed.
What to point it at
The single most common question, answered up front: you give us a target, and optionally your source. They're not the same thing.
| Target (required) | Source (optional) | |
|---|---|---|
| What | Your live, running app — a URL or API endpoint that answers requests right now. | Your code — a git repo or an uploaded .zip. |
| Why | It's what the scanner actually attacks. | Lets the scanner read the code as it attacks (white-box) — deeper findings. |
| Example | https://staging.yourapp.com/chat | github.com/acme/app or my-app.zip |
Staging vs production — always aim at staging
- Production is the real app your customers use, with real data.
- Staging is a separate, running copy in a test environment — the same app, but with no real customers or data.
The scanner is a real attacker: it submits inputs, can create records, trigger actions your app exposes, and drive up model cost against your app. On staging that's harmless. On production it can mean junk data or real side effects for real users. Only scan production if you understand and accept that (a test account and a quiet time help).
How do I know my URL? A staging URL looks like…
It's just a web address where your app runs. Common shapes:
staging.yourapp.comorapp-staging.yourapp.com- A preview URL from Vercel or Netlify (they mint one per change)
- A
*-staging.onrender.comor*.herokuapp.comapp - A local address (
localhost:3000) exposed to the internet with a tunnel (ngrok, cloudflared)
Easiest: let the app find it for you
In the app, paste the one link you already know — your main website or app address — into "Not sure what to scan?" and press Find what to scan. We look at the page, spot the AI features and API endpoints, and check public records for your other environments (like staging. and app.). You get back a short list of recommended targets — click one to scan it. No hosting logins, no hunting.
Find it yourself — no developer needed
Prefer to do it by hand, or want to double-check? You don't need anyone technical. Two reliable ways:
- Open your own app and copy the link. Use your product the way a customer does — go to the page with the AI feature (the chat, the assistant) and copy the address straight from your browser's address bar. That address is your target.
- Follow the money to your hosting account. Your app runs somewhere, and whoever's card pays the hosting bill can see every URL. Check your statements for Vercel, Netlify, Render, Heroku, AWS, DigitalOcean, Google Cloud, log into that account, and the dashboard lists your app's addresses — production and any staging/preview ones.
Backup routes: your domain registrar / DNS (GoDaddy, Namecheap, Cloudflare — wherever you bought the domain) shows subdomains like staging., app., api.; or ask your hosting provider's support from inside your account.
I don't have a staging URL — how do I get one?
- Most hosts spin up a staging copy in one click from your account — Vercel, Netlify, and Render all offer free preview/staging environments, no code required.
- If your app is on GitHub, a preview URL is often generated automatically on each pull request.
- Only have production? You can still scan it — just treat it as a live attack (quiet time, a test account, expect some junk data). See Staging vs production above.
- No one technical at all? A freelancer can stand up staging in an afternoon — then you paste that URL here.
Once you have the URL, follow Run your first scan. Add your code (upload or repo) for a deeper white-box scan.
Black-box vs white-box
How much you give the scanner decides how much it finds.
- Black-box — you give only a live URL or API. The scanner attacks from the outside, like an anonymous attacker. Fast, zero setup, but it can only find what's reachable from the front door.
- White-box — you also give the application's source. The scanner reads the code as it attacks, so it understands the tools, prompts, and data paths behind the endpoint. This is a large uplift in both the number and the depth of findings.
Two ways to hand over the source — you don't need to know git:
- Upload a
.zipof your project. In the app, drop it into the scan box; over the API, upload it and pass the returnedupload_id. Your code is held only for that scan and deleted when it ends — never used to train. - Point at a git repo — GitHub, GitLab, Bitbucket, or Azure DevOps. Paste the repo URL in the
sourcefield, or in the app Connect GitHub once and pick a repo (including private ones) from a dropdown. For other hosts, paste the URL; private repos there use the.zipupload.
Complete scan — both passes, one click
Turn on Complete scan (the checkbox on the scan form) to run both passes for one target automatically: a black-box pass probes the live endpoints, then a white-box pass reads your source and re-tests the known surface. You get one merged findings list, each card tagged by the pass that found it. It needs your source (a repo or .zip) and uses about twice the budget of a single scan.
The white-box pass carries the black-box findings forward and re-tests each one: an issue that's still exploitable is shown as a ↻ re-confirmed regression (counted once, not twice), and anything only the source could reveal is added as new. Even the Free plan can run a Complete scan — capped at $20 total ($10 per pass).
What it tests
Every category on the OWASP LLM Top 10, mapped to MITRE ATLAS:
- Prompt injection
- Sensitive information disclosure
- Supply chain
- Data & model poisoning
- Improper output handling
- Excessive agency (tool & agent abuse)
- System prompt leakage
- Vector & embedding weaknesses (RAG)
- Misinformation
- Unbounded consumption (runaway cost)
The full matrix, with the mapping to MITRE ATLAS, is published on the Trust Center.
Findings & proofs of concept
A finding is a vulnerability the scanner proved, not a guess. Each one carries:
| Field | What it is |
|---|---|
title | One-line summary of the vulnerability. |
severity | critical high medium low — impact-ranked. |
cwe / cvss | Standard weakness ID and CVSS score, when applicable. |
impact | What an attacker gains in plain terms. |
technical_analysis | Why it works, for an engineer. |
remediation | The concrete fix. |
poc | A working, re-runnable proof of concept. Paid plans only. |
endpoint / method / locations | Where it lives — the request and the code locations. |
poc) is held back. Upgrading to any paid plan unlocks it on all findings, including past ones.Reading a finding — the labels explained
Every finding is tagged with standard security labels so you can judge the risk and decide what to fix first. Here is what each label means, how to read it, and why it matters.
| Label | What it is & how to read it | Why it matters |
|---|---|---|
| Severity | critical high medium low — our impact ranking. | Your fix order. Address critical and high first — an attacker can cause real damage. Low is hardening. |
| CVE Common Vulnerabilities & Exposures | A specific, publicly-catalogued flaw in a specific software version — usually a third-party library your app depends on. The ID looks like CVE-2026-27962 (year + number); look it up at nvd.nist.gov for details. | The flaw is public — attackers already know it and often have ready-made exploits. The fix is almost always to upgrade the affected dependency to a patched version. |
| CWE Common Weakness Enumeration | The type of flaw, not a specific instance — e.g. CWE-89 (SQL injection), CWE-918 (SSRF), CWE-862 (missing authorization). | Tells you the kind of mistake, so the standard fix pattern is known. |
| CVSS Common Vulnerability Scoring System | A standardised 0–10 severity score. 9.0–10 critical · 7.0–8.9 high · 4.0–6.9 medium · 0.1–3.9 low. | An industry-standard number to compare and rank risk consistently. |
| OWASP LLM Top 10 | The security industry's standard list of the ten biggest risks specific to AI/LLM apps, coded LLM01–LLM10 (full list below). Every AI-specific finding maps to one. | Puts your risk in a framework auditors, engineers, and insurers recognise. |
| MITRE ATLAS | The attacker's playbook for AI systems — the AI counterpart of MITRE ATT&CK. Each finding maps to the technique an attacker would actually use. | Shows the real-world attack technique behind the finding. |
| PoC Proof of Concept | A working, re-runnable demonstration of the exploit — the exact steps and payload. | The finding is proven, not a guess. If it has a PoC, it is real and reproducible. |
CVE from our automated dependency scan) is a known flaw in a library you use — fix it by upgrading the library. An application finding is a flaw in your own app's logic — prompt injection, broken access control, and so on — fix it by changing your code or configuration.OWASP LLM Top 10 — what each risk means
| Code | Risk | In plain terms |
|---|---|---|
LLM01 | Prompt Injection | Attacker-supplied text overrides the model's instructions or hijacks its tools. |
LLM02 | Sensitive Information Disclosure | The app leaks secrets, personal data, or another user's data. |
LLM03 | Supply Chain | Vulnerable or untrusted dependencies, models, or plugins. |
LLM04 | Data & Model Poisoning | Attacker-controlled data corrupts training, fine-tuning, or the RAG corpus. |
LLM05 | Improper Output Handling | The app trusts model output unsafely — passing it into SQL, a shell, or HTML. |
LLM06 | Excessive Agency | The agent has too much power or access; a manipulated model can act on it. |
LLM07 | System Prompt Leakage | The hidden system prompt — and any secrets or logic in it — can be extracted. |
LLM08 | Vector & Embedding Weaknesses | RAG/vector-store flaws: cross-tenant retrieval, corpus poisoning, embedding leakage. |
LLM09 | Misinformation | False or fabricated output that causes real harm — unsafe reliance, hallucinated code. |
LLM10 | Unbounded Consumption | No limits on use — denial-of-wallet, resource exhaustion, or model extraction. |
Spend caps & safety
Autonomous agents call a language model, which costs money. Two independent limits make sure a scan can never surprise you:
- Per-scan budget cap. Every scan takes a
budget(US dollars, default10). The engine stops when it hits that ceiling. - Token watchdog. A separate backstop kills any scan that blows past a hard token count, even for a self-hosted model the pricing layer can't see. So a runaway loop can't drain your account.
Every scan also runs in its own isolated sandbox, and concurrency is capped per plan so you can't accidentally launch a hundred scans at once.
Authorization & domain verification
Because a scan is a real attack, you may only scan an app you own or are authorized to test. To make that more than a promise, the first time you scan a new public domain we ask you to prove you own it — one time per domain. This stops anyone from pointing us at a competitor.
Verify by either method (pick whichever you can reach — you only need one):
- Meta tag (easiest). Add a line to your homepage's
<head>. Most site builders — Wix, Squarespace, Webflow, Shopify — have a "header code" or "site verification" box for exactly this:<meta name="millenniums-verification" content="YOUR-TOKEN"> - DNS TXT record. Add a TXT record at your domain registrar:
millenniums-verification=YOUR-TOKEN
Verifying the root domain covers all its subdomains — verify acme.com once and you can scan staging.acme.com, api.acme.com, and the rest. You'll also confirm a short authorization certification (that you own the app, or are authorized to test it) before the first scan runs.
localhost, a private IP, host.docker.internal — need no verification. That's your own machine.Verify in the app when prompted, or over the API at POST /api/domains/verify. Discovery (reading a public page) needs no verification — only scanning does.
Your data
Your code and scan results are yours. Each scan runs in a throwaway sandbox that is destroyed when the scan finishes, so we hold as little of your code as possible and only for as long as the scan needs it. We never use your code or results to train any model. Full details, including sub-processors, are in the Trust Center and Privacy Policy.
Traceability report
Every scan produces a chain-of-custody report so you can prove — to yourself, a customer, or an auditor — exactly what happened to your data. It's not a marketing claim; each line is derived from a recorded event in the scan's lifecycle.
The report includes:
- A timestamped timeline: scan created → source received → isolated sandbox launched → scan finished → sandbox torn down → (for uploads) uploaded source deleted.
- Source provenance: whether the scan was black-box (live target only), or white-box from an upload or a git repo. Git URLs are recorded by host only — no paths or credentials.
- Attestations: isolated sandbox, sandbox torn down (verified against the container runtime), uploaded source deleted, spend cap enforced, and never used to train a model.
- Usage: tokens and LLM calls for the run.
Get it in the app on any run under Data trail — chain of custody (with a one-click JSON download), or over the API at GET /api/runs/{run_id}/trace.
Guide: run your first scan
Goal: go from a fresh account to a proven finding.
Prerequisites
- A verified account (browser quickstart).
- A target URL — a live, running instance of your app that you own or may test. Use a staging URL, not production. This is required; your code alone isn't a running app. Don't have one? See What to point it at.
- Optional: your source — a git repo URL or a
.zipupload, for a deeper white-box scan.
Steps
- Open the app and paste your target into the scan box, or call
POST /api/scan. - If you have the source, add it — a repo URL or local path in the
sourcefield. This is the single biggest lever on finding quality. - Start the scan. Note the
run_id. - Wait for it to finish (status
running→false). Minutes, typically. - Open the report and triage from the top: criticals first.
Troubleshooting
- 403, "verify your email" — click the verification link, or hit "Resend" /
POST /api/resend. - 402, quota reached — you've used your included scans. Add a card / upgrade (plans).
- 500, "scan did not start" — the engine couldn't launch (Docker or the model key). For self-hosted, check your
.env; on our hosted app, retry. - Zero findings on a URL-only scan — provide
sourceand re-run. Black-box alone often misses AI-specific paths.
Guide: register a target
If you scan the same app repeatedly, register it once on the Targets tab instead of pasting it in every time. Each target remembers its own settings, so a re-scan is a single click on Scan now. Registered targets are also what the CI plans scan automatically on each pull request.
What each field does
| Field | What it does |
|---|---|
| Name optional | A label so you can recognise the target in the list. Defaults to the target itself. |
| Target | The app to test — a URL, a code repository, an IP address, or a domain. Always aim at staging, never production (see What to point it at). |
| Source repo optional | A link to your code repository. Adding it turns on white-box testing — the scan reads your source, which finds far more real bugs than testing only from the outside (see Black-box vs white-box). |
| Focus / instructions optional | Steer the scan in plain English — the areas to focus on (e.g. “login and payments”), test credentials to sign in with, or specific pages to probe first. |
| Scope | Full scan, or Changed files only — a faster, cheaper re-scan that looks at just what changed. Available when the target is a code repository or you’ve added a Source repo. |
| Base ref changed-files only | What to compare against — a branch, tag, or release. Leave it blank to compare against the repository’s main branch. |
What happens when you click Scan now
- Your settings are checked, and anything that can’t work is caught right away — for example Changed files only needs a code repository, so you’re told before the scan starts rather than after it fails.
- Your plan and spend cap are applied, so a scan can never run past your budget (see Spend caps & safety).
- The test runs in a throwaway, isolated sandbox — your code is never reused or kept (see Your data).
- When it finishes, the results appear under Findings (see Read your scan results).
Guide: read your scan results
When a scan finishes, open it from the Scans list. Its results appear under Findings, organised into tabs so you never have to scroll to reach a section.
The tabs
| Tab | What's in it |
|---|---|
| Findings | Every vulnerability the scan proved, as cards you can expand. Where you'll spend most of your time. |
| Dependency CVEs | Known flaws in your third-party libraries (from an automated dependency scan), kept separate from the app-logic findings. Exploitable ones are auto-checked for reachability — whether your code actually calls the vulnerable path — and the unreachable ones are suppressed, with an OpenVEX export (⬇ VEX) of those determinations for your auditors. |
| Penetration Test Report | The formal written report (produced only when a scan runs to completion). Share it, download a branded PDF, or the raw .md. |
| Data Trail | The chain-of-custody record — proof your code ran only in a throwaway sandbox, was never used to train any model, and was deleted when the scan ended. |
Filter the findings
A large scan can surface hundreds of findings. The coloured pills above the list let you focus — click one to filter, click ✕ clear filter to see everything again:
- Severity pills — Critical High Low — show only findings at that level. A big scan opens focused on the most severe by default, so the worst is in front of you first.
- ⚡ PoC — show only findings that come with a working, proven exploit (the highest-confidence ones).
Every finding is also adversarially re-checked before it's shown: anything the scanner judges a likely false positive is hidden by default (a toggle brings it back so you can review the call yourself).
Read one finding
- Start with Critical and High — those are what an attacker reaches soonest.
- Open "Proof of concept" to see, in plain steps, exactly how the vulnerability is exploited. On paid plans, expand ▸ Exploit script for the actual runnable exploit so an engineer can reproduce it.
- Read "Remediation" — the concrete fix — and the code locations.
- Fix it (see the next guide) and re-scan to confirm the hole is closed.
Guide: fix what the scan found
On the Developer plan and above, with GitHub connected, MillenniumsAI can draft the fixes for you as pull requests — you review and merge; nothing changes on its own. Two one-click actions sit in the top row of a completed scan.
Before you start
- You're on the Developer plan or higher.
- You've connected GitHub (Integrations tab), and the scan used a GitHub repository as its source.
- The scan has completed — a scan that stopped early can be Resumed to completion first.
1. Fix your own code — ⚙ Generate fix PR
For application findings — flaws in your code, like broken access control or injection:
- Open the completed scan and click ⚙ Generate fix PR in the top row.
- It locates the code behind each finding, writes a minimal fix, and opens a draft pull request on your repo.
- Click ↗ View fix PR to review it on GitHub.
- Fix not quite right? Click ↻ Regenerate and tell it what to change or preserve (e.g. "keep the existing session check"). It retries with your guidance and updates the same PR — no duplicates.
2. Fix vulnerable libraries — ⬆ Update dependencies
For dependency findings — known CVEs in the third-party libraries your app relies on:
- Click ⬆ Update dependencies in the top row.
- It bumps each vulnerable package to its patched version in your editable manifests (
requirements.txt,package.json) and opens a draft PR. - Vulnerabilities pinned in a lockfile (
package-lock.json,poetry.lock,uv.lock) can't be safely edited by hand — the PR lists each with the exact command to run (e.g.npm install axios@1.6.0). - Review the changes, run any listed lockfile commands, and merge.
Guide: scan on every pull request
On the Developer plan and up, a scoped scan runs on each pull request that touches your AI surface and blocks the merge on a proven, net-new vulnerability. Recurring findings are de-duplicated, so the pipeline stays quiet until something real appears. On the Developer plan and above, a confirmed finding can also open a draft fix pull request (and a dependency-update PR) your engineers review — nothing merges on its own. Review status is available at GET /api/pr-reviews.
Guide: ask questions about a scan
Not sure what a finding means, or how to fix it in your stack? Ask. POST /api/chat takes a list of messages and, optionally, a run_id so the answer is grounded in that specific scan's findings. Use it to turn a report into a fix plan.
Plans, quota & overage
Pick a monthly plan; add scans as you grow. Full pricing is on the pricing page.
| Plan | Scans | Working PoC | Concurrent | Built for |
|---|---|---|---|---|
| Free | A scan to start | — | 1 | Trying it out |
| Starter | 5 / mo | ✓ | 2 | Solo builders |
| Developer | 15 / mo | ✓ + CI + fix & dependency PRs | 3 | Growing teams |
| Team | 40 / mo | ✓ + CI + fix & dependency PRs | 4 | Shipping on every release |
| Enterprise | Unlimited (fair use) | ✓ On-prem / BYO-key | 8 | Regulated products |
- What's a scan? One run against one target. A pull-request check and a full scan each count as one.
- Overage. On paid plans, scans beyond your monthly quota bill at the posted per-scan rate ($199) rather than blocking you.
- Free tier. A one-time allowance (never resets) so you can see a real finding before you decide. The working exploit and autofix unlock on any paid plan.
Start a self-serve upgrade from the app, or with POST /api/billing/checkout. Enterprise is sales-led — book a walkthrough.
API — Authentication
Every request except signup and the Stripe webhook is authenticated with a bearer token. Your token is created at signup and shown in the app.
Authorization: Bearer YOUR_ACCESS_TOKEN
A missing or unknown token returns 401 Unauthorized. Keep your token secret — it grants full access to your account's scans and findings. If it leaks, rotate it (below) or from Settings in the app.
Passwordless sign-in. Emails a single-use, 15-minute magic link to the address on file. Always returns 200 whether or not an account exists (no account enumeration); rate-limited per network.
| Body | Type | Notes |
|---|---|---|
email | string | Required. The account email. |
Exchange a magic-link token for a fresh access token bound to your account. The token is single-use and expires after 15 minutes. Returns 400 if invalid or expired.
| Body | Type | Notes |
|---|---|---|
token | string | Required. The token from the #login= fragment of the emailed link. |
{ "token": "NEW_ACCESS_TOKEN", "tenant": "acme-1a2b3c", "plan": "free", "verified": true }
Issue a new access token and revoke every previous one — use this if a key leaks. Authenticated with your current token; other signed-in sessions are logged out. Returns { "token": "…" }.
API — Base URL & conventions
- Base URL:
https://scan.millenniums.ai - Content type: requests and responses are JSON. Send
Content-Type: application/jsonon POSTs. - Success:
200 OKwith a JSON body. - Errors: a non-2xx status with
{ "error": "message" }. See status codes.
API — Account
Your account, plan, and quota. The quickest way to confirm a token works.
| Field | Type | Meaning |
|---|---|---|
tenant | string | Your account ID. |
plan | string | free · starter · developer · team · enterprise. |
poc | bool | Whether working PoCs are unlocked on your plan. |
scans_used / scans_limit | int | Usage against your allowance. |
verified | bool | Email verified — required to scan. |
Public. Create an account and get a token. A verification email is sent automatically.
| Body | Type | Required | Notes |
|---|---|---|---|
email | string | yes | Work email preferred. Disposable domains are rejected. |
company | string | no | Used to name your account. |
curl -X POST https://scan.millenniums.ai/api/signup \
-H "Content-Type: application/json" \
-d '{"email":"you@yourcompany.com","company":"Acme"}'
{ "token": "…", "tenant": "acme-3f9a2c", "plan": "free", "verified": false }
Re-send the verification email to your account's address. Requires your token.
API — Scans
Turn one public link into scannable targets. Give the url of your site or app; we fetch the page, detect the AI surface (chat widgets, API/AI endpoints, LLM providers) and stack, and enumerate sibling environments from public certificate-transparency logs. Read-only — it doesn't attack anything.
| Body | Type | Notes |
|---|---|---|
url | string | Required. A public https:// website or app link. |
{ "root_domain": "acme.com",
"ai_surface": [ {"type":"chat-widget","name":"Intercom"}, {"type":"api-endpoint","path":"/api/chat"} ],
"environments": [ "staging.acme.com", "api.acme.com" ],
"recommended_targets": [ "https://acme.com/api/chat", "https://staging.acme.com" ] }
Pick one of recommended_targets and pass it as the target to /api/scan.
Upload your application's source as a .zip for a white-box scan — no git required. The body is the raw zip bytes (Content-Type: application/zip). Returns an upload_id you pass to /api/scan. The upload is stored only for your account, used for that one scan, and deleted when it finishes. Max 100 MB.
curl -X POST https://scan.millenniums.ai/api/upload \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/zip" \
--data-binary @my-app.zip
{ "upload_id": "7Qb2x9Za" }
.zip into the scan box — or send it to whoever manages your code and have them do it.Start a scan against a target. Gated on email verification, your remaining quota, and your plan's concurrency limit.
| Body | Type | Default | Notes |
|---|---|---|---|
target | string | — | Required. A URL, repository, or API endpoint to attack. |
upload_id | string | none | Optional. The id from POST /api/upload — a white-box scan of your uploaded code. Deleted after the scan. |
source | string | none | Optional. A git URL (https:// or git@) to clone for white-box. Use this or upload_id. |
budget | number | 10 | Per-scan spend cap, US dollars. |
mode | string | standard | Scan profile: quick, standard, or deep. |
curl -X POST https://scan.millenniums.ai/api/scan \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"target":"https://staging.yourapp.com/chat",
"source":"https://github.com/acme/chat-app","budget":15}'
{ "run_id": "20260731-abc123" }
run_id immediately; poll GET /api/runs/<id> for progress and results.List your scans, most recent first — each with id, status, running, and findings_count.
One scan in full, including the findings array (see Findings for every field). On free-tier plans the poc field is withheld. Returns 404 if the run isn't yours.
The scan's chain-of-custody report — see Traceability. A timestamped timeline plus attestations proving your code stayed in an isolated sandbox, the sandbox was torn down, and (for uploads) the source was deleted. Every field is derived from a recorded event, not asserted.
{ "run_id": "20260731-abc123",
"timeline": [
{ "ts": "2026-07-31T05:00:00Z", "event": "Isolated sandbox launched", … },
{ "ts": "2026-07-31T05:04:03Z", "event": "Sandbox torn down", "detail": "verified" },
{ "ts": "2026-07-31T05:04:04Z", "event": "Uploaded source deleted", … } ],
"attestations": { "isolated_sandbox": true, "sandbox_torn_down": true,
"uploaded_source_deleted": true, "no_model_training": true } }
| Field | Type | Meaning |
|---|---|---|
id | string | The run ID. |
status | string | running, completed, stopped: token cap, … |
running | bool | true until the scan finishes. |
findings_count | int | How many proven findings. |
findings | array | The findings (full detail on ?full / single-run fetch). |
API — Targets
Saved, named targets you scan repeatedly. These are what the CI plans scan on each pull request.
List your registered targets.
| Body | Type | Default | Notes |
|---|---|---|---|
target | string | — | Required. The URL / repo / API. |
name | string | = target | A friendly label. |
source | string | none | Source for white-box scans. |
instruction | string | none | Plain-English focus for the scan — areas to prioritise, test credentials, or specific endpoints to probe first. |
scope_mode | string | full | One of full, diff, auto. diff scans only files changed since diff_base and requires a git repo (a source, or a repository target) — otherwise returns 400. |
diff_base | string | default branch | With scope_mode: diff, the branch, tag, or commit to compare against. |
API — Domain verification
Prove you own a domain before scanning it — see Authorization & verification. Scanning a public target you haven't verified returns 403 with the token and instructions.
Returns your per-domain token and re-checks ownership (meta tag, then DNS TXT). Pass attest: true to record the authorization certification. Call it once to get the token, add the tag/record, then call it again to confirm.
| Body | Type | Notes |
|---|---|---|
domain | string | Required. The domain (or a URL — we use its root). |
attest | bool | Certify you own it / are authorized to test it. |
{ "domain": "acme.com", "verified": true, "method": "meta",
"meta_tag": "<meta name=\"millenniums-verification\" content=\"…\">",
"dns_txt": "millenniums-verification=…" }
List your domains and their verification status.
API — PR reviews
The status of pull-request scans for your registered targets (Developer plan and up). Each entry reports the PR, whether the scoped scan is clean, and any proven net-new findings that blocked the merge. See the CI guide.
API — Chat
Ask questions in natural language. Pass a run_id to ground the answer in a specific scan.
| Body | Type | Required | Notes |
|---|---|---|---|
messages | array | yes | Chat turns, e.g. [{"role":"user","content":"…"}]. |
run_id | string | no | Grounds the reply in that scan's findings. |
{ "reply": "The prompt-injection finding on /chat lets a user…" }
API — Billing
Start a Stripe Checkout session to upgrade. Returns a hosted checkout url to redirect the user to.
| Body | Type | Notes |
|---|---|---|
plan | string | starter · developer · team. Enterprise is sales-led. |
{ "url": "https://checkout.stripe.com/c/pay/cs_live_…" }
Returns 501 if billing isn't configured on the instance, 400 for an unknown plan.
API — Errors & status codes
Errors return a JSON body { "error": "…" } with one of these statuses:
| Code | Meaning | Common cause |
|---|---|---|
400 | Bad request | Missing target, invalid email, unknown plan. |
401 | Unauthorized | Missing or unknown bearer token. |
402 | Payment required | Scan quota reached — upgrade or add a card. Body includes plan, quota, used. |
403 | Forbidden | Email not verified, or account suspended. |
404 | Not found | Unknown route, or a run that isn't yours. |
429 | Too many requests | Signup rate limit, or your plan's concurrent-scan cap. |
500 | Server error | Scan couldn't start (Docker / model key), or chat failed. |
501 | Not implemented | Billing not configured on this instance. |