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.