Batch Endpoint
Analyze up to 5 Terms of Service items in a single API call.
POST /api/analyze/batch
Send up to 5 Terms of Service URLs or text blobs in one HTTP request. Each item is analyzed independently; partial failures are returned alongside successes.
Required scope
API key: analyze
Limits
- Maximum items per request: 5
- Minimum items per request: 1
- Per-item accounting: each valid item reserves quota independently. Items that cannot reserve quota fail without preventing other items from completing.
- Latency target: ~1.5s per item at p50 on a Base-tier key. Worst-case 5-item batch tolerates up to 8s before Cloudflare Worker timeout.
Request
curl -X POST https://themys.ca/api/analyze/batch \
-H "Authorization: Bearer $THEMYS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"items": [
{"url": "https://stripe.com/terms"},
{"url": "https://vercel.com/legal/terms"},
{"text": "By signing up you agree to..."}
]
}'
Each item may have text or url (one required, both allowed). The optional domain field normalizes the domain display when scanning by text:
{
"items": [
{
"url": "https://stripe.com/terms",
"domain": "stripe.com"
},
{
"text": "By signing up you agree to share your email with our advertising partners.",
"domain": "example.com"
}
]
}
Without domain, items with text show as "(unknown)" on the result card. Recommend always providing domain for text-only items.
Response
200 OK on success (even with partial per-item failures). Items run independently:
{
"results": [
{
"ok": true,
"index": 0,
"cached": false,
"result": {
"riskScore": 6.8,
"riskLevel": "yellow",
"summary": "Detected 5 flagged clauses…",
"flags": [/ ... /],
"tosdr": { "matched": true, "rating": "B" }
}
},
{
"ok": true,
"index": 1,
"cached": true,
"result": { "...": "..." }
},
{
"ok": false,
"index": 2,
"error": "Empty text from https://blocked.com/terms",
"status": 502
}
],
"summary": {
"total": 3,
"succeeded": 2,
"failed": 1,
"authenticatedAs": "api-key"
},
"usage": {
"tier": "base",
"scans": {
"allowed": true,
"remaining": 23,
"limit": 25,
"resetsAt": 1784174400000,
"windowEnd": 1784174400000
},
"ai": {
"allowed": true,
"remaining": 28,
"limit": 30,
"resetsAt": 1785556800000,
"windowEnd": 1785556800000
},
"creditBalance": 0
}
}
Top-level shape:
resultsArrayok, index, and either result or error.summary.totalnumbersummary.succeedednumberok: true.summary.failednumberok: false.summary.authenticatedAs"api-key" \usage.scansobjectusage.aiobjectusage.creditBalancenumber0.Per-item shape
Success:{
"ok": true,
"index": 0,
"cached": false,
"result": { / full AnalysisResult — same shape as POST /api/analyze / }
}
{
"ok": false,
"index": 1,
"error": "Empty text from https://blocked.com/terms",
"status": 502
}
status mirrors the HTTP status the equivalent single-shot call would have returned (e.g. 502 for URL extraction failure, 400 for missing input).
Status codes
200ok: false)400401403analyze scope413500Note: a 200 OK with summary.failed > 0 is success — it means the batch envelope was processed but individual items had errors. Inspect each results[i] to see per-item failure causes.
Cost and rate limits
- Each successful item charges one slot against your per-key daily scan budget (or the browser-user pool for Clerk callers).
- Failed items don't consume budget.
- Quota exhaustion appears as an item-level
429for API-key callers or402for Clerk callers without available overflow credits. - After the batch,
usage.scans.remainingandusage.ai.remainingreflect the post-charge state. - Send an
Idempotency-Keyheader when retrying a request. A completed replay is reported again without consuming the same allowance or credit twice; a failed, refunded attempt may retry provider work and is charged only if that retry succeeds.
Code examples
Python
import os
import requests
API_KEY = os.environ["THEMYS_API_KEY"]
items = [
{"url": "https://stripe.com/legal"},
{"url": "https://vercel.com/legal/terms"},
{"url": "https://github.com/site/terms"},
]
response = requests.post(
"https://themys.ca/api/analyze/batch",
headers={"Authorization": f"Bearer {API_KEY}"},
json={"items": items},
timeout=30,
)
response.raise_for_status()
data = response.json()
for item in data["results"]:
if item["ok"]:
print(f" {item['index']}: {item['result']['riskScore']}/10")
else:
print(f" {item['index']}: FAILED - {item['error']}")
print(f"\n{data['summary']['succeeded']}/{data['summary']['total']} succeeded")
print(f"Remaining scans: {data['usage']['scans']['remaining']}/{data['usage']['scans']['limit']}")
JavaScript
const THEMEYS_API_KEY = process.env.THEMYS_API_KEY;
async function analyzeBatch(items) {
const r = await fetch("https://themys.ca/api/analyze/batch", {
method: "POST",
headers: {
"Authorization": Bearer ${THEMYS_API_KEY},
"Content-Type": "application/json",
},
body: JSON.stringify({ items }),
});
if (!r.ok) {
const err = await r.json();
throw new Error(${r.status}: ${err.error});
}
return r.json();
}
const result = await analyzeBatch([
{ url: "https://stripe.com/legal" },
{ url: "https://vercel.com/legal/terms" },
]);
for (const item of result.results) {
if (item.ok) {
console.log(Item ${item.index}: Risk ${item.result.riskScore});
} else {
console.error(Item ${item.index}: ${item.error});
}
}
console.log(${result.summary.succeeded}/${result.summary.total} succeeded);
When to use vs single-shot
/api/analyze/api/analyze/batch| Async background job that can't tolerate parallel calls | Anything hammering the API
Both endpoints return the same AnalysisResult shape per item. The batch endpoint simply wraps multiple calls with shared rate-limit accounting.
Was this page helpful?