POST /v1/scan
POST https://scanner.milgram.dev/v1/scanAuthorization: Bearer mgs_…The native endpoint. It takes a JSON body, or the text itself as a plain-text body, and answers with a scan object, or with a stream of server-sent events when asked.
JSON body
Section titled “JSON body”Send exactly one of input or messages.
| Field | Type | |
|---|---|---|
input |
string | A text to scan, on its own. |
messages |
array | A conversation, in the OpenAI chat completions shape: each message with its role and content, and the assistant’s tool_calls, tool results, reasoning and refusals. |
tools |
array, optional | The tool definitions the conversation was given, as in chat completions. Only with messages. |
functions |
array, optional | The legacy form of tools. Only with messages. |
kind |
string, optional | What input is, so it is judged as such (below). Default user_message. |
model |
string, optional | nam-shub-v2-20261008 or nam-shub@latest. Any other value is refused. |
stream |
boolean, optional | true for server-sent events. |
notes |
string, optional | A free-form note stored with the scan (where the text came from, say), at most 4 KiB. Never part of the response. |
session |
string, optional | Names a session the stored scan joins, at most 512 bytes, so related scans are grouped in the console. |
Unknown fields are ignored.
A text is judged for what it is: an instruction in a tool result is suspicious where the same words in a
user’s own message may not be. Set kind whenever input is not a user’s message.
kind |
The text is |
|---|---|
user_message |
A message from the user (the default) |
system_message |
A system or developer prompt |
assistant_message |
A model’s reply, reasoning or refusal |
tool_call |
A tool call the model made: name({arguments}) |
tool_result |
What a tool returned |
server_tool_call |
A call to a tool the provider runs itself, such as web search |
server_tool_result |
What such a tool returned |
tool_definition |
A tool’s name, description and schema |
Conversations
Section titled “Conversations”With messages, every part of the conversation is scanned for what it is, then the parts are judged
together, so an attack spread over several turns is found as well as one in a single message. The parts
are:
| Part | Scanned as |
|---|---|
Each text part of a system or developer message |
system_message |
Each text part of a user message (a message without a role counts as user) |
user_message |
An assistant message’s content, refusal and readable reasoning (reasoning, reasoning_content, reasoning_details) |
assistant_message |
Each of its tool_calls, and a legacy function_call |
tool_call |
A tool or function message |
tool_result |
Each entry of tools and functions |
tool_definition |
The response lists every part where something was found, by its position and its location in your request
(see a message). Encrypted reasoning, the provider’s own state, is skipped.
Images, audio, files and other non-text content cannot be scanned, so a request carrying them is refused
with 400 unsupported_content rather than answered for part of it.
Plain-text body
Section titled “Plain-text body”Send Content-Type: text/plain and the text itself as the body: no JSON, no escaping, the best way to send a
large input. kind, model, stream, notes and session go in the query string:
curl "https://scanner.milgram.dev/v1/scan?kind=tool_result" \ -H "Authorization: Bearer $MILGRAM_SCANNER_KEY" \ -H "Content-Type: text/plain" \ --data-binary @page.htmlLimits
Section titled “Limits”| Limit | Value |
|---|---|
| Input | 150 MiB (157,286,400 bytes) of text, or the key’s own maximum if lower |
| JSON body | Three times the input limit, plus 64 KiB for escaping. Send larger inputs as plain text. |
| Messages | 1,024 per request (more is 400 too_many_messages) |
| Parts | 16,384 per request |
| JSON values | 262,144 per request |
| Tool call arguments | Valid JSON, at most 256 KiB each |
| Tool definitions | At most 1,024, and 1 MiB in all |
| Upload | The body must arrive within 15 minutes, and never stall for more than 30 seconds |
| Scan time | At most 6 hours |
Message content parts must be text (or reasoning, which is scanned as such): any other part type, such as
images, audio, files or a refusal part, is refused with 400 unsupported_content, as are tool call
arguments that are not valid JSON.
A scan always covers the whole input: there is no partial verdict. A large input takes time, so stream anything that may not finish in a couple of minutes.