Google's Perspective API stops working after December 31, 2026. Jigsaw closed usage and quota requests in February 2026 and isn't offering migration support. If your forum, comment section or game chat calls commentanalyzer.googleapis.com, that call starts failing on January 1.
Disclosure: we build Paxmod, a content moderation API for real-time chat, so this guide migrates you to Paxmod. The checklist at the end works whichever API you pick.
Short version
Paxmod isn't a drop-in clone of Perspective. The request and response look different. But in most codebases the switch is one function:
| Perspective | Paxmod | |
|---|---|---|
| Endpoint | POST https://commentanalyzer.googleapis.com/v1alpha1/comments:analyze |
POST https://www.paxmod.com/api/v1/text |
| Auth |
?key=KEY query param |
Authorization: Bearer KEY header |
| Text field | comment.text |
message |
| Who and where |
sessionId (optional) |
user_id and context_id (optional, recommended) |
| Attributes |
requestedAttributes on every request |
Categories and thresholds set per project in the console |
| Score you read | attributeScores.TOXICITY.summaryScore.value |
content_moderation.categories["Harassment"].score |
| Decision | You compare scores to thresholds |
result is "flagged" or "not_flagged"
|
| Max length | 20 KB | 5,000 characters (split longer text) |
Use the www host. The apex domain redirects, and plenty of POST clients won't follow it.
Mapping Perspective attributes to Paxmod categories
| Perspective | Paxmod | Notes |
|---|---|---|
| TOXICITY | Harassment, Hate and Discrimination, Profanities | Take the highest, or just use result
|
| SEVERE_TOXICITY | Threatening, Violence, Hate and Discrimination | Take the highest |
| IDENTITY_ATTACK | Hate and Discrimination | Direct match |
| INSULT | Harassment | Harassment is a little broader |
| THREAT | Threatening, Violence | Take the highest |
| PROFANITY | Profanities | Dictionary matcher: 0.8 on a match, 0 otherwise, so keep the threshold below 0.8 |
| SEXUALLY_EXPLICIT | Sexual | Direct match |
| FLIRTATION | none fixed | Write it as a plain-English rule in Advanced mode |
Don't copy your thresholds across. Paxmod's scores come from different models, so a 0.8 in Perspective isn't a 0.8 in Paxmod. Each category has its own threshold (default 0.5, flagged when the score goes above it).
Before and after: curl
Before:
curl -X POST \
"https://commentanalyzer.googleapis.com/v1alpha1/comments:analyze?key=$PERSPECTIVE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"comment": { "text": "you are trash, uninstall" },
"languages": ["en"],
"requestedAttributes": { "TOXICITY": {}, "INSULT": {}, "THREAT": {} }
}'
After:
curl -X POST "https://www.paxmod.com/api/v1/text" \
-H "Authorization: Bearer $PAXMOD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"message": "you are trash, uninstall",
"user_id": "player_123",
"context_id": "lobby_7"
}'
Before and after: Node
Before:
const res = await fetch(
`https://commentanalyzer.googleapis.com/v1alpha1/comments:analyze?key=${process.env.PERSPECTIVE_API_KEY}`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
comment: { text },
languages: ["en"],
requestedAttributes: { TOXICITY: {} },
}),
}
);
const data = await res.json();
const isToxic = data.attributeScores.TOXICITY.summaryScore.value > 0.8;
After:
const res = await fetch("https://www.paxmod.com/api/v1/text", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.PAXMOD_API_KEY}`,
},
body: JSON.stringify({ message: text, user_id: userId, context_id: channelId }),
});
const data = await res.json();
if (data.status === "error") throw new Error(data.error.code);
const isToxic = data.result === "flagged";
const harassment = data.content_moderation.categories["Harassment"]?.score ?? 0;
Before and after: Python
Before:
import os, requests
r = requests.post(
"https://commentanalyzer.googleapis.com/v1alpha1/comments:analyze",
params={"key": os.environ["PERSPECTIVE_API_KEY"]},
json={
"comment": {"text": text},
"languages": ["en"],
"requestedAttributes": {"TOXICITY": {}},
},
timeout=5,
)
is_toxic = r.json()["attributeScores"]["TOXICITY"]["summaryScore"]["value"] > 0.8
After:
import os, requests
r = requests.post(
"https://www.paxmod.com/api/v1/text",
headers={"Authorization": f"Bearer {os.environ['PAXMOD_API_KEY']}"},
json={"message": text, "user_id": user_id, "context_id": channel_id},
timeout=5,
)
data = r.json()
is_toxic = data.get("result") == "flagged"
What the Paxmod response looks like
Example values. You get back the categories enabled on your project:
{
"status": "success",
"id": "5f0c2a8e-1d9b-4c47-9a51-3c1f6f2b7e10",
"service": "pax",
"request": {
"original_message": "you are trash, uninstall",
"processed_message": "you are trash, uninstall",
"timestamp": "2026-10-08T02:00:00.000Z"
},
"result": "flagged",
"reason": "Content flagged for Harassment",
"degraded": false,
"unavailable_categories": [],
"content_moderation": {
"flagged": true,
"categories": {
"Harassment": { "flagged": true, "threshold": 0.5, "score": 0.93 },
"Threatening": { "flagged": false, "threshold": 0.5, "score": 0.04 },
"Personally Identifiable Information": { "flagged": false, "threshold": 0.5, "score": 0.01 }
}
}
}
-
resultisflaggedwhen any enabled category goes above its threshold, andreasonnames which ones. -
degraded: truemeans an upstream classifier was briefly unavailable, and the categories listed inunavailable_categoriesweren't scored on that call. - Errors look like
{"status": "error", "error": {"code": "INVALID_API_KEY", ...}}.
The 6-step migration plan
- Start a trial and create a project. Copy the API key.
- Turn on the categories that match the Perspective attributes you used. For game chat, start with the Gaming chat preset.
-
Swap the request: new URL, Bearer header,
messageplususer_idandcontext_id. -
Swap the parsing: read
content_moderation.categories[...], or act onresult. - Shadow-run until December 31. Send the same messages to both APIs, compare decisions on real traffic, and set thresholds from that. This is the step people skip, and it only works while Perspective is still answering.
- Cut over and delete the Perspective key before January 1, 2027.
What Perspective never did
- Personal information. "where do you live" gets flagged under Personally Identifiable Information. Perspective had no attribute for that, and in kids' game chat it matters more than swearing.
- Game chat. Paxmod runs on custom Minecraft servers and Roblox games, with a Roblox module and a Unity integration. If you run a Minecraft plugin that used Perspective (ChatProtect and AutoMod both can), there's a Minecraft-specific guide.
-
Player-aware moderation. Each message carries a
user_idand acontext_id. The Studio plan adds conversation context, which checks a message against the player's recent history instead of on its own. - Your own rules. Advanced mode checks messages against a policy you write in plain English.
- Harder to dodge. Text is normalised before scoring, including Unicode lookalike characters and hidden zero-width characters.
What it costs
Perspective was free, and Paxmod isn't. Indie is $29/month with 50,000 message credits, and Studio is $149/month with 500,000. Every plan starts with a 14-day free trial that includes 10,000 messages and every feature. Full details are on the pricing page.
Checklist, whatever API you pick
- [ ] Find every call to
commentanalyzer.googleapis.com(code, plugins, config). - [ ] Note which attributes and thresholds each one uses.
- [ ] Check how your code handles errors. Plenty of integrations treat a failed call as "clean", so moderation can quietly stop on January 1.
- [ ] Shadow-run the replacement while Perspective still works.
- [ ] Cut over before December 31.
The full guide, with the mapping table and FAQ, is on paxmod.com.
Top comments (0)