API
AI detection API and MCP server
The checker on this site is a thin client over a public API. Everything it shows you — the layers, the spans, the caveats, the hash — is in the JSON, and the same pipeline is exposed to agents over MCP.
Updated
Quick start
One request, one report
Authenticate with a bearer token. Keys are issued per workspace and carry the workspace’s tier, its credit balance and its rate limit.
curl -X POST https://api.assaylayer.com/v1/assays \
-H "Authorization: Bearer al_live_xxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"kind": "standard",
"input": { "type": "text", "text": "The paragraph you want to check." },
"reference": "cms-article-8412"
}'The TypeScript version, typed against the shared contracts package so the shapes cannot drift:
import type { ApiError, AssayReport, CreateAssayRequest } from "@assaylayer/contracts";
const body: CreateAssayRequest = {
kind: "deep",
input: { type: "text", text: draft },
reference: "cms-article-8412",
};
const response = await fetch("https://api.assaylayer.com/v1/assays", {
method: "POST",
headers: {
authorization: `Bearer ${process.env.ASSAYLAYER_API_KEY}`,
"content-type": "application/json",
},
body: JSON.stringify(body),
});
if (!response.ok) {
const { error } = (await response.json()) as ApiError;
throw new Error(`${error.code}: ${error.message}`);
}
const report = (await response.json()) as AssayReport;
// Origin established beats origin inferred: check this before the score.
if (report.summary?.signal === "provenance") {
await markAsDisclosed(report.id);
} Response
What comes back
The report is one shape everywhere: on screen, in the PDF, over the API and through MCP. This is a real standard-assay response with the long strings trimmed.
{
"id": "asy_8xk2m4q1v7",
"kind": "standard",
"status": "complete",
"inputHash": "b5bef6159c21e2e071d000891d3c05feb7aaba5070919bae2a9a27285f26cda3",
"wordCount": 108,
"language": "en",
"summary": {
"signal": "ai",
"confidence": "medium",
"aiScore": 0.87,
"headline": "Strong AI signals across most sentences."
},
"layers": [
{
"layer": "watermark",
"provider": "anthropic-watermark",
"outcome": "unavailable",
"note": "Anthropic watermark detection is in private preview."
},
{
"layer": "metadata",
"provider": "c2pa",
"outcome": "unavailable",
"note": "Pasted text carries no manifest."
},
{
"layer": "classifier",
"provider": "base-classifier",
"outcome": "found",
"score": 0.87,
"spans": [{ "start": 0, "end": 95, "score": 0.94 }],
"durationMs": 812
}
],
"caveats": [
"Metadata is absent for pasted text.",
"Detectors flag writing by non-native English speakers more often."
],
"creditsCharged": 1,
"reference": "cms-article-8412",
"createdAt": "2026-09-01T09:12:44.000Z",
"completedAt": "2026-09-01T09:12:45.000Z",
"pipelineVersion": "2026.09.1"
} | Field | Type | Notes |
|---|---|---|
| id | string | Stable report id, usable with the get and export endpoints. |
| kind | standard | deep | Deep adds the deep classifier and an agreement score. |
| status | queued | running | complete | failed | Synchronous text assays return complete. |
| inputHash | string(64) | SHA-256 of the normalised input. The text itself is not stored. |
| wordCount | integer | From the normalised text. Under 50, classifiers refuse to score. |
| language | string | Detected language, when one could be guessed. |
| summary.signal | provenance | none | mixed | ai | insufficient | Read this before the score. |
| summary.confidence | low | medium | high | Never present a score without it. |
| summary.aiScore | 0–1 | Document-level classifier score. Present only when a classifier ran. |
| summary.agreement | 0–1 | Deep assays only. 1.00 means both classifiers landed in the same place. |
| summary.headline | string | A sentence for humans. Never asserts authorship. |
| layers[].outcome | found | not_found | unavailable | too_short | error | Unavailable is an answer, not a failure. |
| layers[].spans | array | Character offsets into the normalised text, with a per-span score. |
| caveats | string[] | Travels with the report. Do not drop these when you render it. |
| creditsCharged | integer | What this request cost the workspace. |
| reference | string | Your own id, echoed back and searchable in the workspace. |
| pipelineVersion | string | Bumped whenever providers or thresholds change. |
Endpoints
The surface
| Method | Path | What it does |
|---|---|---|
| POST | /v1/assays | Run an assay on text or a URL. The whole report comes back in the response. |
| POST | /v1/assays/file | Multipart upload. Adds the metadata layer and one credit. |
| GET | /v1/assays/:id | Fetch a report by id. |
| GET | /v1/assays | List a workspace's reports, filtered by kind, signal or your own reference. |
| GET | /v1/assays/:id/export.json | The same report as a JSON file, for your audit trail. |
| GET | /v1/assays/:id/export.pdf | A one-page PDF carrying the input hash, pipeline version and timestamp. |
| GET | /v1/workspaces/:id/balance | Current credit balance for a workspace. |
| GET | /v1/workspaces/:id/ledger | Every credit movement, with its reason. |
| GET | /v1/me | The signed-in user, their workspaces and the current one. |
| GET | /openapi.json | The OpenAPI document this table is generated against. |
The full reference, generated from the same OpenAPI document, lives at api.assaylayer.com/docs.
MCP
The same pipeline, for agents
The Model Context Protocol is a standard way for an AI assistant to call tools that live outside
itself. Assay Layer runs an MCP server over streamable HTTP at mcp.assaylayer.com/mcp,
authenticated with the same bearer key as the REST API, exposing two tools: assay_text, which
runs an assay and returns the signal, confidence, score, caveats and a link to the full report,
and get_assay, which fetches a report by id. The practical use is an agent that checks a draft
it has just been handed — or one it has just written — before it moves on.
Claude Code and Claude Desktop:
claude mcp add assaylayer \
--transport http https://mcp.assaylayer.com/mcp \
--header "Authorization: Bearer al_live_xxxxxxxxxxxxxxxx"Cursor, in .cursor/mcp.json:
{
"mcpServers": {
"assaylayer": {
"url": "https://mcp.assaylayer.com/mcp",
"headers": {
"Authorization": "Bearer al_live_xxxxxxxxxxxxxxxx"
}
}
}
} Limits and cost
Rate limits and credits
| Tier | Rate limit | Notes |
|---|---|---|
| Free | 10 req/min | Anonymous and signed-in checker traffic, counted per IP address |
| Developer | 60 req/min | First paid API tier |
| Growth | 300 req/min | Sustained pipeline use |
| Scale | 1,000 req/min | High-volume ingestion |
| Compliance | 1,000 req/min | Same ceiling as Scale, with retention controls and a signed DPA |
| Operation | Cost | Notes |
|---|---|---|
| Standard assay | 1 credit | Per started 1,000 words. Base classifier, watermark and metadata layers. |
| Deep assay | 10 credits | Per started 1,000 words. Adds the deep classifier and an agreement score. |
| File upload | +1 credit | Added once per file, for text extraction and the C2PA or XMP read. |
Limits are per key, per minute. Going over returns rate_limited with a 429; running out of
credits returns insufficient_credits with a 402, and no assay is charged for. Full prices are
on the pricing page, and the thresholds behind the numbers in the response are on
the accuracy page.
Questions
API questions
How do I get an API key?
Sign in, open the workspace settings and create one. Keys start with al_live_ and the secret is shown once, at creation. Revoking a key takes effect immediately.
Is the API synchronous?
Yes for text. A standard assay returns the finished report in the same response, because that is what a checker in a form needs. File uploads above two megabytes are queued, and you can supply a webhook URL to be called with the finished report.
What happens when a provider is down?
That layer returns an outcome of error after one retry, and the rest of the report is still produced. A missing layer never silently becomes a clean result — the outcome is in the JSON and the summary confidence drops to low.