@saplingai/sapling-js 1.0.52 → 1.0.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,13 +2,13 @@
2
2
 
3
3
  Node.js client for the [Sapling.ai API](https://sapling.ai/api).
4
4
 
5
- Try out a [grammar check demo](https://sapling.ai/grammar-check).
5
+ Try out a [grammar check demo](https://sapling.ai/grammar-check) or an [AI detection demo](https://sapling.ai/ai-content-detector). The [SDK demo](https://sapling.ai/sdk-textarea-demo) shows both running live in a textarea through the browser SDK bundled with this package.
6
6
  Compare against grammar checking tools and APIs like Grammarly (Grammerly), LanguageTool, ProWritingAid and Ginger.
7
7
 
8
8
  ## Functionality
9
9
 
10
- Sapling is an AI messaging assistant.
11
- The API currently offers spelling and grammar checking endpoints.
10
+ Sapling is an AI writing assistant.
11
+ The API offers spelling and grammar checking, AI content detection, rephrasing, tone detection and more; the bundled browser SDK adds in-page grammar suggestions, autocomplete and AI detection to textareas and contenteditables.
12
12
 
13
13
  Benefits include:
14
14
  - **60% more grammar corrections**: Compared to [other systems](https://sapling.ai/comparison/api) at similar accuracy using state-of-the-art generative AI models.
@@ -84,6 +84,24 @@ skips error classification (faster and lighter), and each edit comes back withou
84
84
  client.edits('I have a apple!', undefined, { includeErrorCategories: false, autoApply: true })
85
85
  ```
86
86
 
87
+ `edits()` and `spellcheck()` responses are typed (`EditsResponse`, `SpellcheckResponse`).
88
+ If a model stage can't process some sentences in time, the response still returns
89
+ everything that did complete and adds a `degraded` object. Those sentences may be
90
+ missing some or all of their edits, and the object gives counts only (not which
91
+ sentences), so retry the whole request after a short backoff if you need a
92
+ complete result:
93
+
94
+ ```javascript
95
+ const { data } = await client.edits(text);
96
+ if (data.degraded) {
97
+ // { sentences_degraded: 1, sentences_total: 12, reason: 'model_timeout' }
98
+ // reason: model_timeout | model_unavailable | model_error
99
+ }
100
+ ```
101
+
102
+ If no sentence could be checked, the request rejects with a `503` and a
103
+ `Retry-After` header instead (see [Error handling](#error-handling)).
104
+
87
105
  ### Text classification
88
106
 
89
107
  Zero-shot classification with your own labels (plain names or `{ name, description }` objects, 2–20):
@@ -102,6 +120,219 @@ client
102
120
  })
103
121
  ```
104
122
 
123
+ ### AI content detection
124
+
125
+ ```javascript
126
+ const { data } = await client.aidetect(documentText);
127
+ // { score: 0.05, ai_fraction: 0.51, verdict: 'mixed',
128
+ // segments: [{ start: 0, end: 240, score: 0.96 }],
129
+ // low_confidence: false, low_confidence_reasons: [], text: '...', version: '...',
130
+ // sentence_scores: [{ sentence: '...', score: 0.97, start: 0, end: 112 }, ...],
131
+ // tokens: [...], token_probs: [...] }
132
+ ```
133
+
134
+ `score` (0–1) rates the whole text, so a document that is only partly AI-written can score low;
135
+ `ai_fraction` is the share of the text in sentences scoring ≥ 0.5 (null when nothing could be
136
+ scored), which flags those documents. `verdict` sums both up as `'human'`, `'mixed'` or `'ai'`
137
+ (`'ai'` needs `score` ≥ 0.5 and `ai_fraction` ≥ 0.8; `'mixed'` is anything else with `score` ≥ 0.5
138
+ or `ai_fraction` ≥ 0.2; with a null `ai_fraction` it is `'ai'` or `'human'` by `score` alone), and
139
+ is omitted when the text couldn't be scored. `segments` lists each run
140
+ of consecutive sentences scoring ≥ 0.5 as `{ start, end, score }`, even with `sent_scores: false`.
141
+ `low_confidence` is true for inputs with a higher false-positive rate, named in
142
+ `low_confidence_reasons`: `'short_text'` (under 500 characters) and `'list_format'` (mostly bullet or
143
+ numbered lines); show those results hedged rather than as a bare score. `sentence_scores` use the
144
+ same model and 0.5 threshold, and their `start`/`end` (like a segment's) index `text` in Unicode code points (`end` exclusive) — slice with
145
+ `Array.from(text).slice(start, end).join('')` if the text may contain emoji. A response may also
146
+ carry an advisory `language` (`{ code, name, confidence }`) when the text is confidently
147
+ identified. English, Spanish, Portuguese, French, German, Italian, Dutch and Japanese are
148
+ supported: unpinned text detected as `es`, `pt`, `fr`, `de`, `it`, `nl` or `ja` is scored by a
149
+ multilingual model and answers `version: 'ml20261009'` (that model has no Italian or Dutch training
150
+ data yet, so it catches somewhat less AI-generated text in those two), while other languages are
151
+ scored by the English model and are less reliable. Options: `sent_scores` (default true), `score_string` (HTML
152
+ heatmap), `lang` (`'en'`, `'es'`, `'pt'`, `'fr'`, `'de'`, `'it'`, `'nl'` or `'ja'`, case-insensitive, region
153
+ subtags such as `'pt-BR'` accepted: use that language's model instead of detecting it, which helps
154
+ with short texts; omit it or pass `'auto'` to detect; other languages get a 400), `version` (pin
155
+ a model; a pin always overrides the language routing and `lang`). `language` still reports the
156
+ detected language when `lang` is set, so a mismatch is visible. Pass an array of 1–10 texts for the
157
+ batch form, which answers `{ results: [...] }` in input order; `lang` applies to every item.
158
+
159
+ Pass `zero_retention: true` to keep no copy of the text. The request is still scored and billed,
160
+ but its result is not written to the shared result cache and usage is logged without the text (a
161
+ hash of it is kept so a repeat isn't billed twice). Teams with data retention disabled get this on
162
+ every request. `fileAidetect()` and `urlAidetect()` take the same option.
163
+
164
+ Pass `create_report: true` (single text only, not a batch) to also save the result as a shareable
165
+ report: the response adds `report` (`{ id, url, shared_at, expires_at }`), a public link to the
166
+ text and its scores that you can revoke from your AI detector history and that expires 90 days
167
+ after creation. It needs your private API key, and can't be combined with `zero_retention`
168
+ (both get a 400). When no report can be made (for example, history or text retention is turned
169
+ off, or the history is full), `report` is `null` and `report_skip_reason` says why; the request is
170
+ still scored and billed. `fileAidetect()` and `urlAidetect()` take it too, naming the report after
171
+ the file or the final URL.
172
+
173
+ ```javascript
174
+ const { data } = await client.aidetect(reviewText, { lang: 'pt-BR' }); // version: 'ml20261009'
175
+ ```
176
+
177
+ To score a document without extracting its text yourself, upload it with `fileAidetect()`. It
178
+ accepts `.pdf`, `.docx`, `.pptx`, `.odt`, `.txt`, `.md`, `.html` or `.htm` files up to 30MB, and the
179
+ format is chosen by file extension. The first 200,000 characters of the document's text are scored
180
+ like `aidetect()`, with `sentence_scores` and `score_string` always included. The language is
181
+ detected and routed automatically; there is no `lang` or `version` option, but `zero_retention`
182
+ works as on `aidetect()`. Billing is the same as `aidetect()`.
183
+
184
+ ```javascript
185
+ import { readFileSync } from 'node:fs';
186
+
187
+ const { data } = await client.fileAidetect(readFileSync('essay.pdf'), { filename: 'essay.pdf' });
188
+ // In the browser, pass the File from an <input type="file"> (its own name is used):
189
+ // await client.fileAidetect(input.files[0]);
190
+ ```
191
+
192
+ To score a web page, pass its URL to `urlAidetect()`. The API fetches the page and scores its main
193
+ readable text: navigation, headers, footers, forms and hidden elements are dropped, and the largest
194
+ `<article>` (else `<main>`) is preferred, so a scraped page's boilerplate isn't scored or billed. A
195
+ URL that serves a `.pdf`, `.docx`, `.pptx`, `.odt`, `.txt` or `.md` document is read like
196
+ `fileAidetect()`. Options and billing are the same as `aidetect()`, and the response adds `source`:
197
+ the final URL after redirects, the page title, its content type, the `characters` scored, and
198
+ `truncated` (HTML past 2MB, documents past 15MB and text past 200,000 characters are not scored).
199
+ Only public http(s) URLs on ports 80/443/8080/8443 are fetched, at most 30 per minute per account. A
200
+ page that is fetched but can't be scored (a non-2xx status, unsupported type, no readable text)
201
+ answers 422, and one that can't be fetched (timeout, connection failure, or a site that blocks
202
+ automated requests) answers 502.
203
+
204
+ ```javascript
205
+ const { data } = await client.urlAidetect('https://example.com/blog/post', { sent_scores: false });
206
+ // { score: 0.93, ai_fraction: 0.88, text: '...', version: '...', ...,
207
+ // source: { url: 'https://example.com/blog/post', title: '...', content_type: 'text/html',
208
+ // characters: 5120, truncated: false } }
209
+ ```
210
+
211
+ ### Rephrase, tone, sentiment and readability statistics
212
+
213
+ ```javascript
214
+ const { data: rephrased } = await client.rephrase('hey, can u send it over', {
215
+ mapping: 'informal_to_formal', // default 'paraphrase'
216
+ num_results: 3, // 1–8, default 5
217
+ });
218
+ // { results: [{ hash: '...', original: '...', replacement: 'Could you please send it over?',
219
+ // rephrase_type: 'informal_to_formal', model_version: '...' }, ...] }
220
+
221
+ const { data: tone } = await client.tone('Really stoked about this! Will it be ready by next week?');
222
+ // { overall: [[0.92, 'curious', '🤓'], [0.05, 'excited', '😀'], [0.01, 'confused', '😕']],
223
+ // results: [[[0.99, 'excited', '😀'], ...], [[0.55, 'confused', '😕'], ...]],
224
+ // sents: ['Really stoked about this!', 'Will it be ready by next week?'] }
225
+
226
+ const { data: sentiment } = await client.sentiment('Love the new release. The install was painful.');
227
+ // { overall: [[0.71, 'POSITIVE'], [0.29, 'NEGATIVE']],
228
+ // results: [[[0.99, 'POSITIVE']], [[0.95, 'NEGATIVE'], ...]],
229
+ // sents: ['Love the new release.', 'The install was painful.'] }
230
+
231
+ const { data: stats } = await client.statistics(documentText, { lang: 'en' });
232
+ // { chars: 1234, words: 210, sentences: 12, reading_time_sec: 47.55, reading_time_min: 1,
233
+ // readability: 8.2, readability_scores: { flesch_reading_ease: 64.1, gunning_fog: 10.3, ... } }
234
+ ```
235
+
236
+ `rephrase()` mappings: `paraphrase`, `reword`, `expand`, `shorten`, `summarize`,
237
+ `passive_to_active`, `active_to_passive`, `informal_to_formal`, `formal_to_informal`,
238
+ `sentence_split`, `generate_title`, `generate_acronym`, `generate_meta_description`,
239
+ `switch_tone` (with `tone_mapping`) and `switch_tense` (with `tense_mapping`). Tones come as
240
+ `[probability, tone name, emoji]` triples: `overall` holds up to three for the whole text, most
241
+ likely first, with probabilities summing to 1, and `results` holds one list per sentence. A text
242
+ the API cannot split into sentences gets a 200 with `[]`, and the text is capped at 8000
243
+ characters. Sentiment labels
244
+ are `POSITIVE`, `NEUTRAL` and `NEGATIVE`, as `[probability, label]` pairs, most likely first;
245
+ a text with no sentences (markup or punctuation only) gets a 204 with an empty body (`data === ''`),
246
+ and one the API cannot split into sentences gets a 200 with `[]`.
247
+ `statistics()` is unbilled; `lang` is one of `en`, `de`, `es`, `fr`, `it`, `nl`, `pl`, `ru`, `ar`
248
+ or `auto` and picks the readability formulas (`readability_scores` is empty for `auto`).
249
+
250
+ ### Autocomplete
251
+
252
+ ```javascript
253
+ const { data: completion } = await client.complete('Thanks for getting back to me so');
254
+ if (!Array.isArray(completion)) {
255
+ // { output: ' quickly.', query: 'Thanks for getting back to me so', hash: '...' }
256
+ // When the user takes the suggestion:
257
+ await client.acceptComplete(completion.hash, completion.query, completion.output);
258
+ }
259
+ ```
260
+
261
+ `complete()` needs an API key with autocomplete enabled and a query of at most 200 characters.
262
+ It answers `[]` when it has no completion. `query` in the response is the part of the sent
263
+ query the prediction continues: the API keeps only the text after the last run of three
264
+ spaces. Pass `hash`, `query` and `output` to `acceptComplete()` unchanged, because the API
265
+ checks them against the hash and answers 400 when they don't match.
266
+
267
+ ### Language detection
268
+
269
+ ```javascript
270
+ const { data: detected } = await client.langdetect('Bonjour tout le monde, comment allez-vous ?');
271
+ // { lang: 'fr', conf: 0.99, name: 'French', reliable: true, script: 'Latn',
272
+ // candidates: [{ lang: 'fr', conf: 0.99, name: 'French' }, ...] }
273
+
274
+ const { data: short } = await client.langdetect('Hola');
275
+ // { lang: 'es', conf: 0.95, name: 'Spanish', reliable: false, script: 'Latn', ... }
276
+ ```
277
+
278
+ Act on `lang` only when `reliable` is true. It is true only when the confidence is high and the
279
+ text has enough letters, so short inputs like `"OK"` or `"Hola"` are never reliable however high
280
+ `conf` is. `script` is the text's dominant writing system as an ISO 15924 code: `Latn`, `Cyrl`,
281
+ `Arab`, `Deva`, `Hang`, `Hani` for Chinese, and `Jpan` once the text contains any kana. It is `Zzzz`
282
+ for a script outside the map, and `null` when the text has no letters. Options are `top_k` (1–10
283
+ candidates, default 3) and `segments: true`, which adds a per-sentence `segments` breakdown (`start`,
284
+ `end`, `text` plus the same fields) for mixed-language text. Pass an array of 1–10 texts for the
285
+ batch form, `{ results: [...] }`. Language detection is unbilled.
286
+
287
+ ### Similar terms, profanity and the custom dictionary
288
+
289
+ ```javascript
290
+ const { data: similar } = await client.similarTerms('pet');
291
+ // { neighbors: [[0.81, 'dog'], [0.78, 'cat'], [0.75, 'puppy']],
292
+ // synonyms: ['darling', 'favorite', 'favourite', ...] }
293
+
294
+ const { data: profanity } = await client.profanity('This is so damn stupid.');
295
+ // { toks: ['This', 'is', 'so', 'damn', 'stupid.'], labels: [0, 0, 0, 1, 1] }
296
+
297
+ // Words the grammar and spelling checks should accept (product names, jargon).
298
+ const { data: entry } = await client.addDictionaryEntry('Sapling', { case_sensitive: true });
299
+ // { id: '...', entry: 'Sapling', case_sensitive: true, created_at: '...', updated_at: '...' }
300
+ const { data: entries } = await client.dictionaryEntries(); // a bare array, sorted by entry
301
+ await client.deleteDictionaryEntry(entry.id); // 204, empty body
302
+ ```
303
+
304
+ `similarTerms()` returns up to five `[similarity, term]` neighbors (similarity 0–1, most similar
305
+ first) plus WordNet `synonyms` (multi-word ones use underscores); only the first 100 characters of
306
+ the query are used. `profanity()` splits the text on whitespace and labels each token 1 (profane)
307
+ or 0. Both are unbilled. `addDictionaryEntry()` answers 400 for a duplicate entry or one with more
308
+ than two digits; `deleteDictionaryEntry()` answers 400 (not 404) for an id that is not this
309
+ account's.
310
+
311
+ ### Custom mappings and custom filters
312
+
313
+ ```javascript
314
+ // Always suggest `mapping` wherever `entry` matches (brand names, house style).
315
+ const { data: mapping } = await client.addCustomMapping('sapling', 'Sapling', {
316
+ description: 'Our product name is capitalized.', // shown with the suggestion; it then can't be ignored
317
+ });
318
+ // { id: '...', entry: 'sapling', mapping: 'Sapling', description: '...', case_sensitive: true, ... }
319
+ const { data: { custom_mappings } } = await client.customMappings(); // sorted by entry
320
+ await client.deleteCustomMapping(mapping.id); // 204, empty body
321
+
322
+ // Stop suggesting edits that rewrite `input` to `output`; '' on either side is a wildcard.
323
+ const { data: filter } = await client.addCustomFilter('Patient', 'The patient');
324
+ // { id: '...', input: 'Patient', output: 'The patient', case_sensitive: true, ... }
325
+ const { data: { custom_filters } } = await client.customFilters(); // sorted by input
326
+ await client.deleteCustomFilter(filter.id); // 204, empty body
327
+ ```
328
+
329
+ In a mapping's `entry`, `^` and `$` anchor the match to the start and end of a sentence, and `#`
330
+ matches a number (a `#` in `mapping` puts it back). `case_sensitive` defaults to true. The API
331
+ answers 400 for a duplicate, an entry equal to its mapping (or a filter whose two sides are equal),
332
+ more than two digits in the entry or input, and a case-insensitive rule whose two sides differ only
333
+ in case (`iphone` → `iPhone` must be case sensitive). A filter needs at least one non-blank side.
334
+ Deleting an id that is not this account's answers 400, not 404. All six calls are unbilled.
335
+
105
336
  ### Style-guide rulesets
106
337
 
107
338
  Check text against style rules inline, or save a named ruleset once (owned by
@@ -191,6 +422,47 @@ as `YYYY-MM-DD`, or `'list'`), and every value must be grounded in a quote from
191
422
  extraction, not inference, so a value that isn't stated is reported in `missing` rather than
192
423
  guessed.
193
424
 
425
+ ### Extracting from documents
426
+
427
+ `fileExtract()` runs the same extraction over an uploaded document. It takes the same file types
428
+ and 30MB limit as `fileAidetect()`, and the same `fields` and `context` as `extract()`. Only the
429
+ first 10,000 characters of the document's text are read. The response adds a `document` block that
430
+ says how much text was used; `characters_used` is what is billed.
431
+
432
+ ```javascript
433
+ const { data } = await client.fileExtract(
434
+ readFileSync('invoice.pdf'),
435
+ ['invoice_number', { name: 'total', type: 'number' }, { name: 'due_date', type: 'date' }],
436
+ { filename: 'invoice.pdf', context: 'A vendor invoice' },
437
+ );
438
+ // { data: { invoice_number: 'INV-1042', total: 1299.0, due_date: '2026-03-05' }, fields: [...],
439
+ // missing: [],
440
+ // document: { file_type: 'pdf', characters: 2310, characters_used: 2310, truncated: false } }
441
+ ```
442
+
443
+ When `truncated` is true, fields that appear only after the first 10,000 characters are reported
444
+ in `missing`. To read a later section, extract its text yourself and pass it to `extract()`.
445
+
446
+ ## Document Summarization
447
+
448
+ `fileSummarize()` summarizes an uploaded document: the same `summary`, `key_points` and `length`
449
+ as the API's `/summarize` endpoint, for the same file types and 30MB limit as `fileAidetect()`.
450
+ `length` is `'short'`, `'medium'` (the default) or `'long'`. Only the first 20,000 characters of the
451
+ document's text are read, and the `document` block says how much was used; `characters_used` is
452
+ what is billed.
453
+
454
+ ```javascript
455
+ const { data } = await client.fileSummarize(readFileSync('report.pdf'),
456
+ { filename: 'report.pdf', length: 'short' });
457
+ // { summary: 'Revenue rose 12% ...', key_points: ['...', '...'], length: 'short', result: '...',
458
+ // document: { file_type: 'pdf', characters: 48210, characters_used: 19996, truncated: true } }
459
+ ```
460
+
461
+ All three upload methods take a `File`/`Blob` (in the browser, or Node 18+) or the file's bytes
462
+ (`Buffer`, `Uint8Array`, `ArrayBuffer`). Bytes need `options.filename`, because the extension picks
463
+ the format. File problems come back as 400 `{ error: '...' }` and an oversized upload as 413.
464
+ Uploads are limited to 60 per minute per account across all three methods.
465
+
194
466
  ## Translation
195
467
 
196
468
  Translate text into a target language, named by ISO 639 code (`'fr'`, `'zh-TW'`) or English
@@ -213,12 +485,58 @@ Region variants `zh-CN`/`zh-TW`/`zh-HK`, `pt-BR`/`pt-PT`, `en-GB`/`en-US`, `fr-C
213
485
  text actually is, and `formality` (`'formal'`/`'informal'`) picks the register where the
214
486
  target language distinguishes them.
215
487
 
488
+ ## Inclusive Language
489
+
490
+ Flag non-inclusive or biased wording (gendered defaults, ableist idioms, ageist framing,
491
+ outdated terms for groups) with offsets, a category, a severity and a neutral replacement:
492
+
493
+ ```javascript
494
+ client
495
+ .inclusive('The chairman said the plan was crazy.', { categories: ['gender', 'disability'] })
496
+ .then(function (response) {
497
+ console.log(response.data);
498
+ // { issues: [
499
+ // { category: 'gender', severity: 'low', text: 'The chairman', start: 0, end: 12,
500
+ // note: 'Gendered job title...', suggestion: 'The chair' },
501
+ // { category: 'disability', severity: 'low', text: 'crazy', start: 31, end: 36,
502
+ // note: 'Ableist idiom...', suggestion: 'unworkable' } ],
503
+ // categories: ['gender', 'disability'], inclusive: false }
504
+ })
505
+ ```
506
+
507
+ Categories: `gender`, `race_ethnicity`, `disability`, `age`, `lgbtq`, `religion`,
508
+ `socioeconomic`, `appearance` (default: all). Pass an array of up to 10 texts for the batch
509
+ form (`{ results: [...] }`).
510
+
216
511
  ## Further Documentation
217
512
 
218
513
  - Full Sapling HTTP API and SDK [documentation](https://sapling.ai/docs).
219
514
  - Create a key by following [these instructions](https://sapling.ai/docs/api/api-access).
220
515
  - JavaScript, HTML, and Python [starter code](https://github.com/saplingai/sapling-samples).
221
516
 
517
+ ## Usage reporting
518
+
519
+ Construct the client with `returnUsage: true` and every successful response additionally
520
+ reports the usage the request submitted for metering, as a top-level `usage` field:
521
+
522
+ ```javascript
523
+ const client = new Client(apiKey, { returnUsage: true });
524
+ const response = await client.classify('I was charged twice.', ['billing', 'other']);
525
+ console.log(response.data.usage);
526
+ // { characters: 20 }
527
+ ```
528
+
529
+ `characters` is the count submitted for billing (one count per **distinct** item of a batch
530
+ request; `0` for unbilled requests). It is captured before the billing pipeline's dedup
531
+ window applies, so it is an upper bound on the final charge. Batch responses carry `usage`
532
+ on the `{ results: [...] }` wrapper, not on the per-item results.
533
+ `accountStatus()` and `apiQuotaUsage()` are unaffected: both are unbilled and their responses
534
+ already have their own `usage` field, which the option leaves untouched.
535
+ Responses that are not a JSON object cannot carry the field, so `dictionaryEntries()` (a bare
536
+ array), `deleteDictionaryEntry()`, `deleteCustomMapping()` and `deleteCustomFilter()` (`204`,
537
+ empty body), `sentiment()`'s `204` / `[]` edge cases and the `[]` answers of `tone()` and
538
+ `complete()` never report it.
539
+
222
540
  ## Error handling
223
541
 
224
542
  Every client method returns the axios promise, so API errors (a `400` for
@@ -238,7 +556,9 @@ API error bodies are JSON objects with a human-readable `msg`, for example
238
556
  `{ msg: 'Invalid API key.' }`. The one exception is the 4MB `413`, which uses
239
557
  `message`/`result` keys. When a `402` or `429` body has a numeric `retry_after`,
240
558
  the request can succeed after waiting that many seconds. Without it, retrying
241
- unchanged won't help. See
559
+ unchanged won't help. A `503` means the service is temporarily saturated: wait
560
+ for its `Retry-After` header (`error.response.headers['retry-after']`, in
561
+ seconds), then retry. See
242
562
  [Rate Limits & Errors](https://sapling.ai/docs/rate_limits/).
243
563
 
244
564
  To find out why a key is blocked, and for how long, call `accountStatus()`. It
@@ -264,3 +584,25 @@ account's `subscription`, prepaid `credits`, `usage.chars_this_month` and, on a
264
584
  free trial, `trial`. See [Account Status](https://sapling.ai/docs/api/account-status/).
265
585
  Each call counts as 2,000 characters against the endpoint's own rate-limit
266
586
  bucket, so call it when something fails rather than before every request.
587
+
588
+ ## Usage and quota reports
589
+
590
+ Four unbilled methods report this key's usage over a window of days, set with
591
+ `start_days_back` / `end_days_back` (non-negative integers, days before today):
592
+
593
+ ```javascript
594
+ const { data } = await client.apiUsage({ start_days_back: 30 });
595
+ console.log(data.total_characters, data.total_cost_usd, data.by_endpoint);
596
+ ```
597
+
598
+ - `apiUsage()`: characters and list-price cost by masked key id, endpoint, day and month.
599
+ The window defaults to the current month. A team-scoped key covers the whole team; other
600
+ keys also get `credit_balance_data`.
601
+ - `apiEndpointUsage({ interval })`: characters per endpoint, bucketed `'Daily'`
602
+ (default), `'Weekly'` or `'Monthly'`.
603
+ - `apiQuotaUsage()`: the monthly `quota` (`-1` for none) and the window's `usage`.
604
+ - `apiUserActivity()`: edits shown, accepted and ignored, plus active users. Accepts and
605
+ ignores are only counted if you report them with `acceptEdit()` / `rejectEdit()`.
606
+
607
+ A window over a year gets a `403`. Unlike `accountStatus()`, these need a key that isn't
608
+ blocked. See [API Reporting](https://sapling.ai/docs/api/api-reporting/).
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@saplingai/sapling-js",
3
- "version": "1.0.52",
4
- "description": "JavaScript library for using the Sapling.ai API.",
3
+ "version": "1.0.54",
4
+ "description": "JavaScript client for the Sapling.ai API (grammar checking, AI content detection, rephrasing and more) plus the browser SDK.",
5
5
  "type": "module",
6
6
  "main": "./sapling/client.js",
7
7
  "types": "./sapling/client.d.ts",