Skip to content

POST /v1/scan

POST https://scanner.milgram.dev/v1/scan
Authorization: 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.

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

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.

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:

Terminal window
curl "https://scanner.milgram.dev/v1/scan?kind=tool_result" \
-H "Authorization: Bearer $MILGRAM_SCANNER_KEY" \
-H "Content-Type: text/plain" \
--data-binary @page.html
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.