Google’s Perspective API ends on 31 December 2026. https://api.unprofane.com/v1alpha1/comments:analyze answers the same request with the same response and the same error envelope, so a client moves by changing its base URL and its key.
curl -X POST "https://api.unprofane.com/v1alpha1/comments:analyze?key=up_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"comment":{"text":"you are a clown"},"requestedAttributes":{"TOXICITY":{}},"languages":["en"],"doNotStore":true}'
What is the same
- The path, the request body, the response shape (
attributeScores,summaryScore,spanScores), the error envelope withcode,messageandstatus. - The errors themselves. Before launch we sent Google a request for each way a request can go wrong (the key, the body, each field’s type, the attributes, the languages, the size, the quota), and every answer we give is Google’s, word for word, with the same status. A test replays all of them against the API on every change.
- The production attributes:
TOXICITY,SEVERE_TOXICITY,IDENTITY_ATTACK,INSULT,PROFANITY,THREAT,SEXUALLY_EXPLICIT, and the experimental aliases.FLIRTATIONis answered asSEXUALLY_EXPLICIT, andOBSCENE, whose definition is the same asPROFANITY’s, asPROFANITY. - The key as
?key=(the first one, when a client sends two), as anX-Goog-Api-Keyheader, or as a bearer header with an Unprofane key. - The JSON Google read: any content type, a gzip body, single quotes, bare keys, trailing commas, snake_case field names, a list given as one string, a number given as a string, a byte that is not UTF-8 read as a space, and the same nesting limit.
- Google’s query parameters:
prettyPrint=falsefor compact JSON,fieldsto keep some fields (and Google’s refusal of a mask it cannot read),alt=json(otheraltvalues refused as Google refused them),quotaUserand the$forms. A request field set in the query (?comment.text=…) is bound as Google bound it, and a form body is read as query parameters, as Google read it. - A browser’s preflight, and the CORS headers on every answer to a request that names its origin. The path with a trailing slash, and
X-HTTP-Method-Override. spanScores: one span over the whole comment, or one per sentence withspanAnnotations.scoreThresholdleaves out an attribute under it,scoreTypetakesPROBABILITYandRAW, andclientTokencomes back.doNotStoreis honoured because nothing is stored either way.comments:suggestscoreis accepted and answered with{}, as Google answered it.
What is different
- Scores come from a different engine. The rules give each attribute a floor from the matched words; the model’s reading lifts the rest. The replay tool takes a CSV of your texts with their old scores and maps your thresholds onto ours; it runs in memory and stores nothing.
- With
spanAnnotations, each sentence’s span is scored by the rules on that sentence alone; the summary score is the whole engine’s. - A byte that is not UTF-8, or half of an escaped emoji pair, is a space to Google and a masked letter to our engine: a word broken by one (“di\xffkhead”) is read as the word, where Google’s reading let it through. What the response shows (the accepted request, the offsets) is Google’s.
- A
RAWscore is the same number as the probability: the engine has no separate raw scale. - The other New York Times attributes (
ATTACK_ON_AUTHOR,ATTACK_ON_COMMENTER,INCOHERENT,INFLAMMATORY,LIKELY_TO_REJECT,SPAM,UNSUBSTANTIAL) and the bridging attributes (AFFINITY_EXPERIMENTAL,COMPASSION_EXPERIMENTAL,CURIOSITY_EXPERIMENTAL,NUANCE_EXPERIMENTAL,PERSONAL_STORY_EXPERIMENTAL,REASONING_EXPERIMENTAL,RESPECT_EXPERIMENTAL) are not scored: a request for one answers the error Google gives an attribute it does not know (Unknown requested attribute), or leaves it out whendropUnsupportedAttributesis set. - Where Google refused and we can answer, we answer: a comment in a language Google detected but did not score (Hebrew, Turkish and the rest are scored), a comment Google found no language in at all (a run of one letter, an escaped half of an emoji pair), unless it is blank,
SEXUALLY_EXPLICITin every language the other attributes take (Google scored it in English only), and anHTMLcomment (Google refused it; we score the text without its tags). Languages you name inlanguagesare held to Google’s list, as Google held them. - A secret key sent from a browser (a page’s
fetch) is refused withPERMISSION_DENIED, where Google answered: in a page, the key is visible to every visitor. Call from your server. - A query string Google’s front end found too long (a 411 and its HTML page) is read.
- A body larger than the route’s byte limit is refused before it is read, where Google read far larger ones to say the comment was too long.
- A spent minute answers Google’s quota error with your project in place of Google’s project number and our plans as the place to ask for more. A spent month or a spent daily test cap answers
RESOURCE_EXHAUSTEDin our own words, since Google had neither. - A wrong path or method answers 404, as Google’s did, with a JSON body where Google sent its HTML error page.
- Our own failure answers
500 INTERNALwith Google’s standard words for its own. Google’s other account errors (a key restricted to a referrer or an address, the API not enabled, billing) have no counterpart: a key here is valid or it is not. - Every response also carries
Unprofane-EngineandUnprofane-Routeheaders, and the usage counts as one check per request. - Until the Civil Comments calibration ships, the scores are the engine’s own scale rather than a fit to Perspective’s. The replay tool says which is in force.