# Voice profile JSON schema: the 45 attributes as returned by the API · ScriptGrain

> The shape of a ScriptGrain voice profile as GET /v1/profiles/{id} returns it: status, confidence, narrative and the 45-attribute object, field by field with JSON types, units and allowed values.

Canonical: https://scriptgrain.com/reference/voice-profile-json-schema

# Voice profile JSON schema: 45 attributes across 8 layers, as the API returns them

*By Jack Stovell · published 2026-09-21 · checked 2026-09-20*

A voice profile is one JSON object of 45 named attributes across eight layers, returned by the API and the MCP server. Fields are typed as numbers, strings, string arrays, share objects and booleans. Here's the schema, field by field, plus how to read it and use it downstream.

## The response shape

Here's the thing about the response: it's one envelope, and the shape doesn't change whether you hit the API directly or call it through MCP. `GET /v1/profiles/{id}` returns a `data` object and a `meta` object. Inside `data` you get `id`, `name`, and `status`, which is one of "processing", "complete" or "failed". While it's running, `progress_percent` sits somewhere between 5 and 95. Once it's done, you get `confidence_score` (0 to 1), `sample_count`, `total_word_count`, and `english_variant`, which is either "uk" or "us".

There's also `created_at`, a `narrative` field (a prose description of the voice, written for humans), and `attributes`, which holds the 45 fields this page is mostly about. `meta` just carries a `request_id`. That's the whole envelope.

Types are straightforward once you see them. Numbers are JSON numbers. Labels are strings. Lists are arrays of strings. Shares are objects of numbers. `pronoun_distribution` might read `{ "I": 0.48, "we": 0.22, "you": 0.30 }`. `the_a_an_ratio` might read `{ "the": 0.48, "a": 0.38, "an": 0.13 }`. Yes/no fields are booleans, plain and simple.

One wrinkle worth flagging: profiles extracted before 2026-08-17 sometimes hold descriptive text in fields that are meant to be labels. The scorer tolerates that, so nothing breaks. But new extractions write canonical labels, so if you're building against fresh profiles you won't hit this at all. The MCP tool `get_profile` returns the identical object, so anything you build against the REST response works unchanged through MCP.

## The attributes object, field by field

The 45 fields sit in eight layers, and the grouping isn't arbitrary. It reflects how the extraction actually works: two passes, one per sample and one synthesis across all samples, each layer capturing a different kind of pattern.

Lexical covers six fields: `vocabulary_diversity_index` (0 to 1), `preferred_words`, `word_length_distribution` (short 1 to 4 letters, medium 5 to 7, long 8+), `rare_word_rate`, `filler_phrases`, and `contraction_frequency`.

Syntactic covers seven: `avg_sentence_length`, `sentence_length_variance`, `complexity_preference`, `clause_ordering`, `parenthetical_rate`, `dash_frequency`, `semicolon_frequency`.

Tone and register covers six: `formality_score`, `humour_register`, `emotional_expressiveness`, `contraction_rate`, `confidence_vs_hedging`, `audience_adaptation`.

Rhetorical covers six: `opening_style`, `closing_pattern`, `metaphor_usage_rate`, `repetition_as_emphasis`, `transition_style`, `argument_structure`.

Punctuation and format covers six: `comma_density`, `exclamation_rate`, `ellipsis_usage`, `question_mark_in_body`, `capitalisation_quirks`, `list_preference`.

Function words covers five: `the_a_an_ratio`, `but_however_yet_preference`, `pronoun_distribution`, `discourse_markers`, `paragraph_opener_words`.

Content patterns covers four: `specificity_level`, `anecdote_usage_rate`, `claim_density`, `analogy_preference`.

Quirks and cadence covers five: `consistent_misspellings`, `rhythm_pattern`, `paragraph_length_preference`, `power_sentence_position`, `structural_signatures`.

That's 45. The table attached to this section lists each field against its layer, type and unit, so you don't have to hold all of it in your head. For the plain-English meaning of each attribute, and what a high or low value actually implies about someone's writing, see [Writing voice attributes](https://scriptgrain.com/reference/writing-voice-attributes).

Field · JSON type · Layer · Unit or values
vocabulary_diversity_index · number · Lexical · 0 to 1
preferred_words · string[] · Lexical · recurring distinctive words
word_length_distribution · object of number shares · Lexical · short (1 to 4 letters) / medium (5 to 7) / long (8+)
rare_word_rate · number · Lexical · rare words per 1,000
filler_phrases · string[] · Lexical · catalogued fillers
contraction_frequency · number · Lexical · contractions per 1,000 words
avg_sentence_length · number · Syntactic · words
sentence_length_variance · number · Syntactic · variance of sentence lengths
complexity_preference · string · Syntactic · simple / compound / complex / mixed
clause_ordering · string · Syntactic · front-loaded / build-to-point / mixed
parenthetical_rate · number · Syntactic · per 300 words
dash_frequency · number · Syntactic · per 1,000 words
semicolon_frequency · number · Syntactic · per 1,000 words
formality_score · number · Tone and register · 0 to 10
humour_register · string · Tone and register · dry / sarcastic / self-deprecating / warm / none
emotional_expressiveness · string · Tone and register · low / medium / high
contraction_rate · number · Tone and register · rate
confidence_vs_hedging · number · Tone and register · 0 (heavy hedger) to 1 (declarative)
audience_adaptation · boolean · Tone and register · register shifts by audience
opening_style · string · Rhetorical · hook / context / direct / anecdote / question
closing_pattern · string · Rhetorical · how pieces end
metaphor_usage_rate · number · Rhetorical · per 1,000 words
repetition_as_emphasis · boolean · Rhetorical · repeats for effect
transition_style · string · Rhetorical · how paragraphs connect
argument_structure · string · Rhetorical · evidence-first / conclusion-first / narrative
comma_density · number · Punctuation and format · per sentence
exclamation_rate · number · Punctuation and format · per 1,000 words
ellipsis_usage · string · Punctuation and format · frequent / occasional / rare / never
question_mark_in_body · boolean · Punctuation and format · questions in body copy
capitalisation_quirks · string · Punctuation and format · any noted
list_preference · string · Punctuation and format · bullets / numbered / inline / mixed / avoids
the_a_an_ratio · object of number shares · Function words · the / a / an
but_however_yet_preference · string · Function words · but / however / yet / mixed
pronoun_distribution · object of number shares · Function words · I / we / you
discourse_markers · string[] · Function words · connecting phrases
paragraph_opener_words · string[] · Function words · recurring first words
specificity_level · string · Content patterns · abstract / balanced / data-driven
anecdote_usage_rate · number · Content patterns · per 1,000 words
claim_density · number · Content patterns · claims per paragraph
analogy_preference · boolean · Content patterns · reaches for analogies
consistent_misspellings · string[] · Quirks and cadence · habitual spellings
rhythm_pattern · string · Quirks and cadence · punchy / flowing / mixed
paragraph_length_preference · string · Quirks and cadence · short / medium / long / varied
power_sentence_position · string · Quirks and cadence · start / end / both / varied
structural_signatures · string · Quirks and cadence · notable patterns

## Types and units

So, the units. Rates are per 1,000 words unless the field says otherwise. `rare_word_rate`, `metaphor_usage_rate`, `anecdote_usage_rate`, `exclamation_rate`, `dash_frequency`, `semicolon_frequency`, `contraction_frequency`: all per 1,000. `parenthetical_rate` is the one exception in that family, measured per 300 words rather than per 1,000, so watch that when you're comparing it against the others.

`formality_score` runs 0 to 10. `confidence_vs_hedging` runs 0 to 1, where 0 is a heavy hedger and 1 is fully declarative. `vocabulary_diversity_index` also runs 0 to 1. `comma_density` is per sentence, not per thousand words, which trips people up if they're skimming.

Labels like `humour_register`, `opening_style`, `complexity_preference` and `clause_ordering` are enumerated strings. `humour_register` takes dry, sarcastic, self-deprecating, warm or none. `opening_style` takes hook, context, direct, anecdote or question. Booleans like `repetition_as_emphasis`, `analogy_preference` and `audience_adaptation` are exactly that: true or false, no middle setting.

Share objects deserve their own mention. `pronoun_distribution` and `the_a_an_ratio` are the two you'll see most, and their values sum to roughly 1 across the object's keys. Treat them as proportions, not counts. If `pronoun_distribution` shows `we` at 0.22, that's 22% of the counted pronouns, not 22 uses per thousand words.

## Reading confidence_score

A low `confidence_score` almost always means one of two things: too few samples, or samples that are too short, or samples pulled from mixed genres that don't share a consistent voice. Sometimes it's all three at once. The product recommends three or more pieces and roughly 3,000 words total for extraction, and that recommendation exists because of what the two-pass process needs to work with.

Pass one reads each sample separately and measures it. Pass two synthesises one profile from everything pass one found. A single 300-word blog excerpt will still produce a profile (the minimum is one sample of 50+ words), but the synthesis has almost nothing to reconcile, so confidence sits low and the numbers can be noisy. Feed it three pieces spanning 3,000 words and pass two has actual disagreement to resolve, patterns that hold across samples versus patterns that were just one piece having an odd day. That's what confidence is really measuring: consistency across samples, not correctness of any single one.

Extraction itself takes roughly one to three minutes regardless of sample count, so there's no time cost to doing this properly. If your `confidence_score` comes back low, the fix is almost never to retry the same samples. It's to add more of them, and to try to keep them within one genre if you can.

## Using the attributes downstream

Here's the thing about consuming this data: you shouldn't reimplement scoring yourself. `/v1/voice-match` exists precisely so you don't have to write your own distance function across 45 heterogeneous fields, some of which are shares, some booleans, some enumerated strings with no natural ordering. Send it a profile id and the text to score, and let it do the comparison. Full detail on that endpoint, including request and response shape, lives at [Brand voice API](https://scriptgrain.com/reference/brand-voice-api).

If you're building a system prompt from a profile rather than scoring against one, the counted fields are your raw material. `avg_sentence_length`, `contraction_rate`, `formality_score`, `humour_register`, `opening_style`, `closing_pattern`: these translate almost directly into instructions. The narrative field is worth including too. It's already prose, already synthesised, and it often captures interactions between attributes that a flat list of numbers won't show on its own.

If you're running this across an agency or a team, store profiles per client rather than per project. Voice tends to belong to the client, not the individual piece of content, and a per-client profile lets you keep sample counts climbing over time, which only helps confidence. For the tool-level mechanics of calling any of this through an agent or assistant, see the [MCP tool reference](https://scriptgrain.com/reference/mcp-tool-reference); for the broader picture of how the API and MCP server fit together, see [AI writing API and MCP server](https://scriptgrain.com/ai-writing-api-and-mcp-server).

## Limits

The schema describes the fields, not their accuracy: the labelled attributes are a model"s reading of the samples and can differ between extractions of the same text, and every rate is only as stable as the sample size behind it. Older profiles may carry descriptive text in label fields. The example values on this page are illustrative shares, not a real person's profile."

## Questions

### What's the minimum data needed to generate a voice profile?

One sample of 50 or more words will produce a profile. It won't be a strong one. Confidence depends on volume and consistency, and a single short sample gives pass two almost nothing to synthesise against. The product recommends three or more pieces and about 3,000 words total, which gives the extraction enough material to separate genuine patterns from one-off quirks.

### Why does parenthetical_rate use a different unit from the other rate fields?

Most rate fields in the schema are counted per 1,000 words, but `parenthetical_rate` is measured per 300 words instead. It's a deliberate exception, not an inconsistency. Parentheticals are rarer events than, say, commas or contractions, so a per-300 window keeps the resulting numbers legible rather than producing tiny decimals across most real samples.

### Can I compare two profiles' share objects directly?

Yes, and that's exactly what `/v1/voice-match` does for you. Share objects like `pronoun_distribution` and `the_a_an_ratio` sum to roughly 1, so comparing them means comparing distributions, not raw counts. You can eyeball two profiles' share objects side by side, but for anything scored or automated, use the match endpoint with the text rather than writing your own comparison logic.

### Does the schema change between the REST API and MCP?

No. `get_profile` through MCP returns the same object as `GET /v1/profiles/{id}` through the REST API: same envelope, same 45 attributes, same types. If you've built logic against one, it works unchanged against the other. That consistency is deliberate, so you can prototype through MCP and move to direct API calls later without touching your parsing code.

### What should I do if confidence_score stays low after adding samples?

Check genre consistency first. Mixed sources (a technical document alongside a casual social post) can keep confidence low even with plenty of words, because pass two is finding real disagreement between samples rather than noise. Narrow the samples to one genre or register, keep the total around 3,000 words or more, and confidence should climb. If it doesn't, the voice itself may genuinely vary by context, which the `audience_adaptation` field will usually confirm.

## Methodology

Field names, types and values are read from the profile schema and the api-v1 response as of 2026-09-20 (ScriptGrain repository); the example values are illustrative shares, not a real person's profile.

## Sources

- [ScriptGrain: Writing voice can be measured (the 45 attributes)](https://scriptgrain.com/writing-voice)
- [ScriptGrain API reference (v1)](https://scriptgrain.com/docs/api)
- [ScriptGrain research index (SGR studies)](https://scriptgrain.com/research)
