Introduction#
The ArticleGrade API runs the same dual-LLM audit that powers the dashboard, from your own code. Point it at a live URL and it returns a 0–100 quality score with a structured breakdown: E-E-A-T and factual depth (graded by Claude), SEO and structure (screened by Gemini), plus the specific phrases that hurt the page and how to fix them.
It is a REST API over HTTPS. Every request and response body is JSON (the one exception is the streaming endpoint, which returns Server-Sent Events). All paths below are relative to the base URL.
https://app.articlegrade.com — prepend it to every path. The current version prefix is /v1.Audits are asynchronous: you start one and get back an id immediately, then either poll for the result or subscribe to the stream. A deep audit typically completes in well under a minute.
Authentication#
Authenticate every request with an API key sent as a bearer token:
Authorization: Bearer ag_live_xxxxxxxxxxxxxxxxxxxxAs an alternative, you may pass the key in the x-api-key header instead — useful for clients that reserve Authorization for something else:
x-api-key: ag_live_xxxxxxxxxxxxxxxxxxxxKeys are created and revoked on the Developers page of your dashboard. Live keys are prefixed ag_live_.
403.Quotas & metering#
Audits are metered against your plan's monthly quota. A fast screen (the Gemini SEO/structure pass) is cheap; a deep audit (the Claude dual-LLM pass) is what counts against your included allotment.
- Deep audits beyond your plan's included amount are billed as overage at your plan's per-audit rate — there is no surprise tier jump.
- When a hard limit is reached (for example a spend cap you've configured, or an unpaid balance), the API returns
402and stops accepting new audits until the limit clears. - Call
GET /v1/accountat any time to read your currentincluded,usedandoveragecounts.
Errors#
Errors use conventional HTTP status codes and a uniform JSON body:
{ "error": "a short, human-readable message" }| 200 | Success. The audit record or list is in the body. |
| 202 | Accepted. The audit (or bulk batch) was queued; the body contains its id(s). |
| 400 | Bad input — missing or malformed url, an invalid provider, or a body that isn't valid JSON. |
| 401 | Missing or invalid API key. |
| 402 | Quota or hard limit hit. Add overage budget or raise your cap, then retry. |
| 403 | Your plan doesn't include this feature (e.g. bulk, schedules, or API access at all). |
| 404 | No audit with that id belongs to your account. |
| 502 | An upstream LLM or fetch failed transiently. Safe to retry with backoff. |
Start an audit#
Queues an audit for a single URL and returns immediately. By default this runs the deep dual-LLM audit. Use provider:"gemini" for just the fast SEO/structure screen, or runBoth:true to force the full deep audit explicitly.
Request body
| Field | Type | Description |
|---|---|---|
| urlrequired | string | The fully-qualified https:// URL of the live page to audit. |
| provideroptional | "claude" | "gemini" | Defaults to "claude" (the deep audit). "gemini" runs the fast SEO/structure screen only — not a deep audit. |
| runBothoptional | boolean | When true, runs the full deep dual-LLM audit (Claude + Gemini). Equivalent to the default deep path. |
curl -X POST https://app.articlegrade.com/v1/audits \
-H "Authorization: Bearer ag_live_xxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.com/blog/best-running-shoes" }'Response 202
{
"id": "aud_8Fq2kR1xR7",
"status": "queued",
"url": "https://example.com/blog/best-running-shoes"
}Take the returned id and poll the audit (or stream it) until status is done or error.
Get an audit#
Fetches the full audit record. Poll this until status is done (or error). While running, score and most of record are null.
Response fields
| Field | Type | Description |
|---|---|---|
| id | string | The audit id, e.g. aud_…. |
| status | string | One of queued, running, done, error. |
| score | number | null | Overall quality score 0–100. null until the audit finishes. |
| provider | string | The provider used for this run (claude or gemini). |
| title | string | The audited page's title. |
| live_url | string | The URL that was fetched and audited. |
| record | object | The full result. See the sub-fields below. |
| record.dims | array | Scored dimensions: [{ name, val }]. |
| record.errors | array | Specific issues: [{ phrase, sev, fix, source }]. |
| record.scores | object | Per-model scores: { claude, gemini }. |
| record.text | string | The extracted body text that was graded. |
| record.headings | array | The page's heading outline. |
| pass | boolean | null | Whether the score met threshold (if one applies), else null. |
| threshold | number | null | The pass/fail threshold applied, if any. |
curl https://app.articlegrade.com/v1/audits/aud_8Fq2kR1xR7 \
-H "Authorization: Bearer ag_live_xxxxxxxxxxxxxxxxxxxx"{
"id": "aud_8Fq2kR1xR7",
"status": "done",
"score": 82,
"provider": "claude",
"title": "The Best Running Shoes of 2026",
"live_url": "https://example.com/blog/best-running-shoes",
"record": {
"dims": [
{ "name": "Experience", "val": 78 },
{ "name": "Expertise", "val": 85 },
{ "name": "Factual depth", "val": 80 }
],
"errors": [
{
"phrase": "experts agree these are the best",
"sev": "high",
"fix": "Attribute to a named source or cite testing data.",
"source": "claude"
}
],
"scores": { "claude": 82, "gemini": 76 },
"text": "Choosing a running shoe comes down to…",
"headings": ["How we tested", "Top picks"]
},
"pass": true,
"threshold": 70
}Stream progress#
Opens a Server-Sent Events (text/event-stream) connection that pushes live progress while the audit runs — an alternative to polling. Two event types are emitted:
status— fired as the audit moves through its phases (fetch, screen, deep pass). Data carries the currentstatus.complete— fired once when the audit reachesdoneorerror; data carries the final score. The stream then closes.
curl -N https://app.articlegrade.com/v1/audits/aud_8Fq2kR1xR7/stream \
-H "Authorization: Bearer ag_live_xxxxxxxxxxxxxxxxxxxx"event: status
data: {"status":"running"}
event: complete
data: {"status":"done","score":82}List audits#
Returns your most recent audits, newest first.
Query parameters
| Param | Type | Description |
|---|---|---|
| limitoptional | integer | How many to return. Defaults to 50. |
curl "https://app.articlegrade.com/v1/audits?limit=50" \
-H "Authorization: Bearer ag_live_xxxxxxxxxxxxxxxxxxxx"Bulk submit#
Submits many URLs in one call. Each accepted URL becomes its own audit you then poll or stream individually. Up to 200 URLs per request.
Request body
| Field | Type | Description |
|---|---|---|
| urlsrequired | string[] | Array of https:// URLs to audit. Maximum 200. |
curl -X POST https://app.articlegrade.com/v1/audits/bulk \
-H "Authorization: Bearer ag_live_xxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "urls": ["https://example.com/a", "https://example.com/b"] }'Response 202
{
"accepted": [
{ "id": "aud_aa11", "url": "https://example.com/a" }
],
"rejected": [
{ "url": "not-a-url", "reason": "invalid url" }
]
}Account & usage#
Returns your organization, current usage against quota, and the feature flags your plan enables — handy for showing remaining budget or deciding whether a feature is available before you call it.
curl https://app.articlegrade.com/v1/account \
-H "Authorization: Bearer ag_live_xxxxxxxxxxxxxxxxxxxx"{
"org": "Acme Media",
"usage": { "included": 200, "used": 147, "overage": 0 },
"features": { "api": true, "bulk": true, "schedules": true, "webhooks": true }
}Schedules#
Creates a recurring audit so a URL is re-graded automatically on a cadence — pair it with a webhook to get alerted whenever a tracked page's score drops.
Request body
| Field | Type | Description |
|---|---|---|
| urlrequired | string | The https:// URL to re-audit on the cadence. |
| cadencerequired | "daily" | "weekly" | "monthly" | How often the audit runs. |
curl -X POST https://app.articlegrade.com/v1/schedules \
-H "Authorization: Bearer ag_live_xxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.com/pillar", "cadence": "weekly" }'Webhooks#
Growth+
Register a webhook endpoint on the Developers page and ArticleGrade will POST it a JSON payload the moment an audit completes — no polling required.
Payload
{
"event": "audit.completed",
"data": {
"id": "aud_8Fq2kR1xR7",
"url": "https://example.com/blog/best-running-shoes",
"score": 82,
"status": "done"
},
"ts": 1782000000
}Headers
| Header | Description |
|---|---|
| X-AG-Event | The event name, e.g. audit.completed. |
| X-AG-Signature | sha256=<hex> — an HMAC-SHA256 of the raw request body, keyed with your endpoint's signing secret (shown once when you create the webhook). |
Verifying the signature
Compute the HMAC over the raw, unparsed body and compare it to the header in constant time. In Node:
const crypto = require('crypto');
function verify(rawBody, header, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
const received = (header || '').replace('sha256=', '');
// timingSafeEqual throws on length mismatch — guard first
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(received, 'hex');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Express — capture the RAW body for verification:
// app.post('/ag-webhook', express.raw({ type: 'application/json' }),
// (req, res) => {
// if (!verify(req.body, req.get('X-AG-Signature'), SECRET))
// return res.sendStatus(401);
// const evt = JSON.parse(req.body.toString());
// res.sendStatus(200);
// });Recipe: CI quality gate#
Fail a build when a page scores below your threshold. This script starts an audit, polls until it finishes, and exits non-zero if the score is under 70 — drop it into any CI step.
#!/usr/bin/env bash
set -euo pipefail
URL="$1"
THRESHOLD=70
AUTH="Authorization: Bearer $AG_API_KEY"
BASE="https://app.articlegrade.com"
# 1. start the audit, capture the id
ID=$(curl -sS -X POST "$BASE/v1/audits" \
-H "$AUTH" -H "Content-Type: application/json" \
-d "{\"url\":\"$URL\"}" | jq -r '.id')
# 2. poll until the audit is done
while :; do
RES=$(curl -sS "$BASE/v1/audits/$ID" -H "$AUTH")
STATUS=$(echo "$RES" | jq -r '.status')
[ "$STATUS" = "done" ] && break
[ "$STATUS" = "error" ] && { echo "audit failed"; exit 1; }
sleep 3
done
# 3. gate on the score
SCORE=$(echo "$RES" | jq '.score')
echo "ArticleGrade score: $SCORE (threshold $THRESHOLD)"
if [ "$SCORE" -lt "$THRESHOLD" ]; then
echo "::error::content quality below threshold — blocking publish"
exit 1
fitest "$(curl -sS "$BASE/v1/audits/$ID" -H "$AUTH" | jq '.score')" -ge 70That's the whole surface. The machine-readable contract lives in openapi.json — import it into Postman, Insomnia, or your codegen of choice.