Style Guide Compliance API
The styleguide endpoint checks a text for compliance with style rules you define in the request — house style, brand voice, editorial guidelines, terminology policy. Send the text together with 1–20 rule names (optionally with a short description of each), and the endpoint returns every violation it finds: the verbatim offending passage with its character offsets, the violated rule, a brief note, and a rewrite suggestion that complies with your rules.
Only the rules you list are enforced — the endpoint never volunteers grammar, factuality or style opinions outside them. For general grammar and spelling, see the edits endpoint; for a generic writing-quality rubric, see quality.
You can also define your house style once as a saved ruleset and check against it
by name ("ruleset": "House Style") instead of re-sending the rules on every request.
Every reported passage is grounded: it is quoted verbatim from your text and located in it, so you
can highlight or auto-fix violations directly from the offsets. The text is checked exactly as
submitted (markup is not stripped — your rules may legitimately govern formatting), which means
start/end always index the string you sent.
Sample Code
- cURL
- JavaScript
- Python
- Python (Sapling client)
curl -X POST https://api.sapling.ai/api/v1/styleguide \
-H "Content-Type: application/json" \
-d '{"key":"<api-key>", "text":"Our synergy-driven solution was leveraged by the team. It is really great!!", "rules": ["no corporate jargon", "active voice", {"name": "no exclamation marks", "description": "Never use exclamation marks, even single ones."}]}'
import axios from 'axios';
async function run(text) {
try {
const response = await axios.post(
'https://api.sapling.ai/api/v1/styleguide',
{
key: '<api-key>',
text,
rules: [
'no corporate jargon',
'active voice',
{name: 'no exclamation marks',
description: 'Never use exclamation marks, even single ones.'},
],
},
);
const {status, data} = response;
console.log({status});
console.log(JSON.stringify(data, null, 4));
} catch (err) {
const msg = err.response?.data?.msg || err.message;
console.log({err: msg});
}
}
run('Our synergy-driven solution was leveraged by the team. It is really great!!');
import requests
from pprint import pprint
response = requests.post(
"https://api.sapling.ai/api/v1/styleguide",
json={
"key": "<api-key>",
"text": "Our synergy-driven solution was leveraged by the team. It is really great!!",
"rules": [
"no corporate jargon",
"active voice",
{"name": "no exclamation marks",
"description": "Never use exclamation marks, even single ones."},
],
}
)
if 200 <= response.status_code < 300:
pprint(response.json())
else:
print('Error: ', response.status_code, response.text)
from sapling import SaplingClient
api_key = '<api-key>'
client = SaplingClient(api_key=api_key)
rules = [
'no corporate jargon',
'active voice',
{'name': 'no exclamation marks',
'description': 'Never use exclamation marks, even single ones.'},
]
result = client.styleguide(
'Our synergy-driven solution was leveraged by the team. It is really great!!',
rules=rules,
)
if result['compliant']:
print('Text complies with the style guide.')
else:
for violation in result['violations']:
print(f"[{violation['rule']}] {violation['text']!r} -> {violation['suggestion']!r}")
print(f" {violation['note']} (chars {violation['start']}-{violation['end']})")
Sample Response
{
"violations": [
{
"rule": "no corporate jargon",
"text": "synergy-driven solution",
"start": 4,
"end": 27,
"note": "\"Synergy-driven\" is a corporate buzzword.",
"suggestion": "effective product"
},
{
"rule": "active voice",
"text": "was leveraged by the team",
"start": 28,
"end": 53,
"note": "Passive construction (and \"leveraged\" is itself jargon).",
"suggestion": "the team used"
},
{
"rule": "no exclamation marks",
"text": "It is really great!!",
"start": 55,
"end": 75,
"note": "Uses exclamation marks.",
"suggestion": "It is really great."
}
],
"rules": ["no corporate jargon", "active voice", "no exclamation marks"],
"compliant": false
}
A text with no violations returns "violations": [] and "compliant": true — that is a successful,
billable check, not an error.
Batch Requests
To check many short texts against the same style guide — a queue of support replies, a set of product
descriptions — in one request, send texts (a list of 1–10 strings) instead of text. The same
rules apply to every item, and the combined length of all items may be up to 10,000 characters (the
same cap as a single text). Exactly one of text and texts must be provided.
curl -X POST https://api.sapling.ai/api/v1/styleguide \
-H "Content-Type: application/json" \
-d '{"key":"<api-key>", "texts": ["Our synergy-driven solution!!", "The team shipped the feature."], "rules": ["no corporate jargon", "no exclamation marks"]}'
The batch response is {"results": [...]} with one entry per input, in input order; each entry has
exactly the single-response shape above, and each entry's violation offsets index that item's own
text:
{
"results": [
{
"violations": [
{
"rule": "no corporate jargon",
"text": "synergy-driven solution",
"start": 4,
"end": 27,
"note": "\"Synergy-driven\" is a corporate buzzword.",
"suggestion": "effective product"
},
{
"rule": "no exclamation marks",
"text": "Our synergy-driven solution!!",
"start": 0,
"end": 29,
"note": "Uses exclamation marks.",
"suggestion": "Our synergy-driven solution."
}
],
"rules": ["no corporate jargon", "no exclamation marks"],
"compliant": false
},
{
"violations": [],
"rules": ["no corporate jargon", "no exclamation marks"],
"compliant": true
}
]
}
Items are billed individually (a batch of N costs the same as N single requests), and each item is
cached the same way as a single request — batches and single calls share the cache. If any item fails
to process, the whole request returns a 502 and nothing is billed; the items that did succeed are
already cached, so a retry only re-runs the failures.
Saved Rulesets
Instead of re-sending your rules on every check, save them once as a named ruleset and pass
"ruleset": "<name>" in place of "rules" on the check endpoint (both the single and the batch
form). A resolved ruleset behaves exactly as if you had sent its rules inline — same model behavior,
same caching, same billing — so the two forms are interchangeable per request.
Rulesets belong to your API key's account: keys scoped to a team share one team-wide namespace (define the house style once, every caller of the team's keys can check against it), and other keys use their user's personal namespace. Names are matched case-insensitively, and each account can hold up to 100 rulesets. Managing rulesets requires a per-account API key and is not billed; it also works while the key is otherwise blocked (e.g. depleted credits) — it is account configuration, not usage.
Create or replace (POST) — the name is the identity (case-insensitive), so posting an existing
name replaces its rules:
curl -X POST https://api.sapling.ai/api/v1/styleguide/rulesets \
-H "Content-Type: application/json" \
-d '{"key":"<api-key>", "name": "House Style", "rules": ["no corporate jargon", {"name": "no exclamation marks", "description": "Never use exclamation marks, even single ones."}]}'
{
"ruleset": {
"name": "House Style",
"rules": [
{"name": "no corporate jargon", "description": ""},
{"name": "no exclamation marks", "description": "Never use exclamation marks, even single ones."}
],
"created_at": "2026-09-23T12:00:00",
"updated_at": "2026-09-23T12:00:00"
},
"created": true
}
name is up to 100 characters (surrounding and duplicate whitespace is normalized); rules has
exactly the shape and bounds of the check endpoint's rules parameter and is stored normalized.
created is false when an existing ruleset was replaced.
List (GET):
curl "https://api.sapling.ai/api/v1/styleguide/rulesets?key=<api-key>"
{"rulesets": [{"name": "House Style", "rules": [...], "created_at": "...", "updated_at": "..."}]}
Delete (DELETE) — the name may ride the query string (some HTTP stacks drop DELETE bodies) or
a JSON body:
# By query string
curl -X DELETE "https://api.sapling.ai/api/v1/styleguide/rulesets?key=<api-key>&name=House%20Style"
# By JSON body
curl -X DELETE https://api.sapling.ai/api/v1/styleguide/rulesets \
-H "Content-Type: application/json" \
-d '{"key":"<api-key>", "name": "House Style"}'
{"deleted": "House Style"}
Deleting a name that does not exist returns a 404. All three operations return a 400 for the
shared public demo credential — saved rulesets need a per-account key. They also return a 403 for
a key's public (client-side) form: listing, saving and deleting rulesets require the private API key,
since a public key ships in browser code. Checking text against a saved ruleset still works with
either form.
Checking against a saved ruleset — pass ruleset in place of rules (exactly one of the two):
curl -X POST https://api.sapling.ai/api/v1/styleguide \
-H "Content-Type: application/json" \
-d '{"key":"<api-key>", "text": "Our synergy-driven solution!!", "ruleset": "House Style"}'
An unknown ruleset name returns a 404 ({"msg": "No saved ruleset named \"...\"."}) and nothing is
billed. The response is identical to the inline form, with rules echoing the saved rule names.
Request Parameters
POST to https://api.sapling.ai/api/v1/styleguide
key: String
32-character API key. Can also be supplied via the Authorization header as a bearer token; if both are provided, the key parameter takes precedence.
text: String
The text to check, up to 10,000 characters. Checked exactly as submitted — markup is not stripped
(style rules may govern formatting), so the returned offsets always index this string. Empty or
whitespace-only text, or text containing a NUL character (U+0000), returns a 400 — NUL cannot be
stripped without shifting the returned offsets. The text is treated as untrusted data: instructions
inside it are ignored and it is judged on its content alone. Provide exactly one of text and
texts.
texts: List[String]
Batch form (see Batch Requests): 1–10 texts to check in one request, each treated
exactly like text above, with a combined length of up to 10,000 characters. An empty item, or an
item containing a NUL character, returns a 400 naming its index. The response becomes
{"results": [...]}, one entry per item in input order, each item's offsets indexing that item's own
text.
rules: List[String | Object]
The style rules to enforce: between 1 and 20 entries. Each entry is either a string (the rule name) or an
object {"name": "...", "description": "..."} where description is optional. Names are trimmed, internal
whitespace is collapsed, and they must be at most 80 characters and case-insensitively unique; descriptions
are at most 400 characters and carry the nuance the model follows (what the rule requires or forbids, and
any exceptions). Names are returned in this normalized form in the response, so match your application
logic on the normalized names. Provide exactly one of rules and ruleset.
ruleset: String
The name of a saved ruleset to check against, in place of inline rules. Matched
case-insensitively within your API key's account (its team, for team-scoped keys); an unknown name
returns a 404 and nothing is billed. Provide exactly one of rules and ruleset.
Response Parameters
violations: List[Object]
Up to 20 violations, most important first. May be empty (the text complies). Each has:
rule: the violated rule's name — always one of the submitted rule names (as normalized).text: the offending passage, quoted verbatim from the submitted text (at most 200 characters).start/end: the passage's character offsets in the submitted text (textequals the exact slicesubmitted_text[start:end]). Offsets count Unicode code points (Pythonstrindexing) — not UTF-16 code units or bytes — so in JavaScript slice withArray.from(text)if the input may contain characters outside the Basic Multilingual Plane (e.g. emoji), whose UTF-16 string indices would otherwise misalign. Both arenullin the rare case the passage cannot be located in the submitted text; the violation is still real andtextstill shows the passage.note: one short sentence explaining how the passage violates the rule.suggestion: a rewrite of the passage that complies with all the rules. May be an empty string when no rewrite applies (e.g. the fix is a deletion).
When the same problem repeats many times, the first few occurrences are reported and the note says so.
rules: List[String]
The checked rule names, normalized (trimmed, internal whitespace collapsed), in the order submitted.
compliant: Boolean
true exactly when violations is empty.
Errors
| Status | Meaning |
|---|---|
400 | Validation error — missing, empty, oversized or NUL-containing text (or a texts item, named by index), both or neither of text/texts, both or neither of rules/ruleset, or invalid rules (e.g. {"msg": "Invalid rules: Needs between 1 and 20 rules."}). The body is {"msg": "..."}. |
401 / 403 | Missing or invalid API key. |
404 | ruleset names no saved ruleset in your account ({"msg": "No saved ruleset named \"...\"."}). Nothing is billed. |
429 | Rate limit exceeded or key over capacity. |
502 | {"msg": "Unexpected error checking text against the style guide."} — the checking model failed; safe to retry. Not billed and not cached (on the batch form, the whole request: see Batch Requests). |
Tips
- Describe your rules. Plain names work for common conventions (
active voice,no exclamation marks), but adescriptionis what carries YOUR house style: what counts as jargon for you, which spellings you standardize on, the exceptions ("Use sentence case in headings, except for product names"). - One rule per concern. Violations name the rule they break, so separate rules (
no jargon,serial comma,spell out numbers under ten) give you per-rule reporting and let editors filter — a single mega-rule collapses everything into one bucket. - Auto-fix from the offsets.
textis always the exactstart:endslice of what you sent, so you can splicesuggestionover[start, end)directly (apply from the last violation to the first so earlier offsets stay valid). Offsets are Unicode code-point indices, so in JavaScript index overArray.from(text)rather than the raw string when the input may contain non-BMP characters like emoji. Skip violations whose offsets arenull. - A terminology policy works well as rules — e.g.
{"name": "product naming", "description": "The product is 'Acme Studio', never 'the studio' or 'AcmeStudio'"}. For mechanical find-and-replace pairs, custom mappings on the edits endpoint are cheaper. - Results are cached on Sapling's side for about three days, keyed on the text and the full rule set (names and descriptions) — re-running the same document against the same rules is fast and returns the same violations; changing any rule re-checks.