API reference
Sounday REST API reference - rewrite and style-check endpoints, parameters, responses, errors and limits.
Base URL: https://sounday.ai/v1. Requests and responses are JSON. Authenticate with
Authorization: Bearer sd_... (or x-api-key: sd_...). Texts are processed in memory and never stored or logged.
POST /v1/rewrite
Rewrites plain text or Markdown to sound human and verifies the facts. Counts the input's words against your monthly quota (failed requests are not charged).
| Field | Type | Default | Notes |
|---|---|---|---|
text |
string | required | Plain text or Markdown. Max words per request depends on your plan. |
strength |
light | standard | strong |
standard |
How much to change. light only removes AI tells. |
keep |
string[] | [] |
Up to 50 terms kept verbatim (case-insensitive match), e.g. SEO keywords, product names. |
voice |
string | Short description of the voice, e.g. "friendly, direct, British spelling". | |
voice_samples |
string[] | [] |
Up to 3 samples of writing to match (style only, never copied). |
rewrite_headings |
boolean | false |
Headings are left untouched unless this is true. |
verify |
boolean | true |
Run the second-model meaning check (number/name/date checks always run). |
on_fail |
original | best_effort |
original |
What to return for a chunk that fails verification twice. |
Example response (illustrative values):
{
"id": "rw_8f3c2a91d0b74e6512aa",
"text": "Our 3 plans start at $19. ...",
"facts_preserved": true,
"checks": {
"chunks": 1,
"chunks_rewritten": 1,
"chunks_reverted": 0,
"protected_spans": 2,
"facts_tracked": 2,
"issues": []
},
"style": {
"before": {
"score": 41,
"verdict": "formulaic",
"findings": 4,
"sentence_length_cv": 0.12,
"em_dashes": 1
},
"after": { "score": 92, "verdict": "natural", "findings": 0, "sentence_length_cv": 0.48, "em_dashes": 0 }
},
"changes": [{ "op": "replace", "before": "In today's fast-paced world, ...", "after": "Our 3 plans ..." }],
"model": "anthropic.claude-opus-5-5",
"usage": { "words": 17, "month_words_used": 1203, "month_words_limit": 10000 }
}
facts_preserved is true when every returned chunk passed verification. With on_fail: "original" a failing chunk
comes back unchanged (counted in chunks_reverted, reasons in checks.issues), so facts_preserved stays true.
What is protected (swapped for placeholders before the model sees the text, restored after): Markdown links and
images, bare URLs, emails, inline code, HTML tags and keep terms. Never sent to the model at all: fenced code
blocks, headings (unless rewrite_headings), tables and YAML front matter.
POST /v1/check
Deterministic style check ("AI tells"). No key needed; with a key the rate limit is per account instead of per IP. Same rules and score as the free checker and the CLI.
curl https://sounday.ai/v1/check -H "Content-Type: application/json" -d '{"text": "..."}'
Returns score (0-100, higher is more natural), verdict (natural, mixed, formulaic, too-short),
findings (each with rule_id, label, suggestion, start, end, text; offsets are Unicode code points),
sentence_length_cv, opening_repeats, em_dashes and lists_of_three. It is a style analysis, not an AI detector.
Keys and account
| Endpoint | Auth | What it does |
|---|---|---|
POST /v1/keys |
none | {"email": "...", "accept_terms": true} creates a free account and returns api_key once. |
POST /v1/keys/rotate |
key | Returns a new key; the old one stops working immediately. |
GET /v1/me |
key | Plan, words used this month, limits, billing status. |
POST /v1/billing/checkout |
key | {"plan": "builder" | "scale"} returns a Stripe Checkout url. |
POST /v1/billing/portal |
key | Returns a Stripe Customer Portal url (change plan, cancel, invoices). |
Errors
Errors are JSON: {"error": {"code": "quota_exceeded", "message": "..."}}.
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_text, invalid_option, invalid_json |
Fix the request. |
| 401 | missing_key, invalid_key |
Send a valid key. |
| 402 | quota_exceeded |
Monthly words used up; upgrade_url included. |
| 413 | text_too_long |
Over your plan's words per request. Split the text. |
| 429 | rate_limited |
Over your plan's requests per minute. Retry after a few seconds. |
| 502 | model_unavailable |
The model provider failed. You were not charged; retry. |
| 503 | free_capacity, billing_not_open |
Free-tier daily capacity reached, or paid plans not open yet. |
Limits
| Plan | Words / month | Words / request | Requests / minute |
|---|---|---|---|
| Free | 10,000 | 1,500 | 10 |
| Builder | 300,000 | 5,000 | 60 |
| Scale | 1,000,000 | 10,000 | 120 |
Words are counted on the input, split on whitespace. Long documents are processed in chunks of about 350 words in parallel; a 1,500-word post usually returns in 15 to 40 seconds. Set your HTTP timeout to at least 60 seconds.