# Sapling.ai Developer Documentation > API and SDK documentation for Sapling.ai: grammar/spell checking, autocomplete, AI content detection, paraphrasing, and language tools via HTTP API and JavaScript/Python SDKs. ## docs API Overview   - [Welcome](https://sapling.ai/docs.md): API Overview   ### api Overview of the Sapling.ai API offerings. - [API Overview](https://sapling.ai/docs/api.md): Overview of the Sapling.ai API offerings. - [Account Status](https://sapling.ai/docs/api/account-status.md): One-call self-diagnosis for a Sapling API key — whether it can make billable calls right now, why not, and when a block clears. - [Advanced Edits](https://sapling.ai/docs/api/advanced-edits.md): Sapling provides customizable proofreading settings that go beyond regular spelling and grammar checking. These include suggestions around making text simpler and more confident, as well as edits around DEI. The Advanced Edits settings are configurable for your account (here) as well as on a per API call level (documented below) because these suggestions may not be applicable to all styles of writing. Advanced Edits are currently only available for English. - [Analysis](https://sapling.ai/docs/api/analysis.md): The Analysis endpoints compute syntactic information about the input text. Outputs include: - [API Access](https://sapling.ai/docs/api/api-access.md): Anyone can test out Sapling's API. To get started with the API, simply: - [API Reporting](https://sapling.ai/docs/api/api-reporting.md): Sapling's API endpoints for retrieving API usage analytics, plus the - [Applying Edits](https://sapling.ai/docs/api/applying-edits.md): After you've sent the Sapling API text and gotten JSON edits in the response, how do you apply the edits to get updated text? - [Autocomplete](https://sapling.ai/docs/api/autocomplete.md): Sapling's autocomplete provides predictions of the next few characters or words - [Chunking](https://sapling.ai/docs/api/chunk.md): Also refered to as segmentation, chunking text allows you to parallelize - [Text Classification API](https://sapling.ai/docs/api/classify.md): The classify endpoint sorts a piece of text into labels you define in the request — no training data, - [5 Best Grammar and Spell Checking APIs / SDKs](https://sapling.ai/docs/api/comparison.md): Looking for a grammar check API? A comparison of 5 grammar and spell checking APIs — Bing, Ginger, LanguageTool, ProWritingAid, and Sapling — with features, latency, and drawbacks of each. - [advanced_edits](https://sapling.ai/docs/api/components/advanced_edits.md): Sample Response - [ai_detector](https://sapling.ai/docs/api/components/ai_detector.md): Sample Code - [apply_edit_javascript](https://sapling.ai/docs/api/components/apply_edit_javascript.md): Develop AI-powered messaging applications using Sapling.ai's developer platform. Includes grammar/spelling correction and autocomplete through HTTP API or JavaScript SDK. - [apply_edit_php](https://sapling.ai/docs/api/components/apply_edit_php.md): Develop AI-powered messaging applications using Sapling.ai's developer platform. Includes grammar/spelling correction and autocomplete through HTTP API or JavaScript SDK. - [apply_edit_python](https://sapling.ai/docs/api/components/apply_edit_python.md): Develop AI-powered messaging applications using Sapling.ai's developer platform. Includes grammar/spelling correction and autocomplete through HTTP API or JavaScript SDK. - [apply_edit](https://sapling.ai/docs/api/components/apply_edit.md): Develop AI-powered messaging applications using Sapling.ai's developer platform. Includes grammar/spelling correction and autocomplete through HTTP API or JavaScript SDK. - [chunk_file_python](https://sapling.ai/docs/api/components/chunk_file_python.md): Develop AI-powered messaging applications using Sapling.ai's developer platform. Includes grammar/spelling correction and autocomplete through HTTP API or JavaScript SDK. - [dei_edits](https://sapling.ai/docs/api/components/dei_edits.md): Sample Code - [tone](https://sapling.ai/docs/api/components/tone.md): Sample Code - [Custom Mappings](https://sapling.ai/docs/api/custom-mappings.md): Custom Mappings can be used to enforce specific corrections or capitalization for the following purposes: - [AI Detector API](https://sapling.ai/docs/api/detector.md): Sapling's AI detector API scores how likely text is AI-generated, at the document, sentence, and token level. REST endpoint plus Python and JavaScript SDKs. - [Dictionary](https://sapling.ai/docs/api/dictionary.md): Add terms such as industry-specific jargon or product names to your dictionary so that Sapling will ignore these terms. The dictionary can also be managed in your web dashboard. - [Grammar Check API](https://sapling.ai/docs/api/edits-overview.md): Sapling's grammar check API returns grammar, spelling, and style corrections as JSON edits with character offsets and error types. REST endpoint plus Python and JavaScript SDKs. - [Error Categories](https://sapling.ai/docs/api/error-categories.md): Error categories are returned as part of the Edits API. They provide additional classification and detail on the types of edits that Sapling suggests. These categories can be used for analysis, reporting, or coaching purposes. - [Benchmarking & Evaluation](https://sapling.ai/docs/api/evaluation.md): How do you determine whether one NLP system that suggests edits - [Data Extraction API](https://sapling.ai/docs/api/extract.md): The extract endpoint pulls fields you name out of unstructured text — invoices, emails, resumes, - [Files](https://sapling.ai/docs/api/files.md): In addition to plain text, Sapling also supports PDF and DOCX processing - [Handling Fragments](https://sapling.ai/docs/api/fragments.md): Sometimes the text in requests may not be full sentences but instead - [Guardrails](https://sapling.ai/docs/api/guardrails.md): Despite their amazing capabilities, LLMs can often behave in undesired - [Inclusive Language API](https://sapling.ai/docs/api/inclusive.md): The Inclusive Language endpoint finds non-inclusive or biased wording in a text: gendered defaults - [Language Detection API](https://sapling.ai/docs/api/langdetect.md): The language detection endpoint identifies the language of a given text and returns a confidence score. - [Named-Entity Recognition API](https://sapling.ai/docs/api/ner.md): The NER endpoint finds named entities in a text — people, organizations, locations, dates, times, - [5 Best Paraphrase APIs](https://sapling.ai/docs/api/paraphrase-comparison.md): Looking for a paraphrasing API? Here's a comparison of 5 different API services along with the features, benefits, and drawbacks of each service. - [PII Detection & Redaction API](https://sapling.ai/docs/api/pii.md): The PII endpoint finds personally identifiable information in a text — email addresses, phone numbers, - [Postprocessing](https://sapling.ai/docs/api/postprocess.md): The Postprocess endpoint performs a variety of operations that are useful - [Preprocessing Overview](https://sapling.ai/docs/api/preprocess-overview.md): Ingestion + Extraction - [Profanity Filter API](https://sapling.ai/docs/api/profanity.md): The profanity API checks for profanity in the given text - it provides a 0/1 label for each token, where 1 indicates profanity was detected. - [Quality Score](https://sapling.ai/docs/api/quality.md): The quality endpoint computes a writing-quality score for a given text. - [Rephrase](https://sapling.ai/docs/api/rephrase.md): The Paraphrase and Rephrase endpoints offer paraphrasing for particular styles. - [Content Safety API](https://sapling.ai/docs/api/safety.md): The content safety endpoint scores a text on seven moderation categories — - [SCIM Provisioning](https://sapling.ai/docs/api/scim.md): Provision and deprovision Sapling Enterprise users from Okta, Microsoft Entra ID (Azure AD), or another SCIM 2.0 identity provider. - [Semantic Search](https://sapling.ai/docs/api/semsearch.md): Semantic search provides the ability to query/search over a set of documents based off the - [Sentiment Analysis](https://sapling.ai/docs/api/sentiment.md): The sentiment endpoint checks the sentiment for a given text - positive, negative, or neutral. - [SEO Analysis API](https://sapling.ai/docs/api/seo.md): The SEO endpoint analyzes a page or article and returns everything needed to publish it well: - [Similar Terms](https://sapling.ai/docs/api/similar-terms.md): Many times you may need to find terms that have a similar meaning to another term. - [Plain Language Simplification API](https://sapling.ai/docs/api/simplify.md): The simplify endpoint rewrites a text in plain language at a reading level you choose — for - [Service Level Agreement (“SLA”)](https://sapling.ai/docs/api/sla.md): Definitions - [Spellcheck](https://sapling.ai/docs/api/spellcheck.md): For some use cases, grammar and phrase-level edits may not be necessary. - [Statistics](https://sapling.ai/docs/api/statistics.md): The Statistics endpoint computes simple statistics for a given text document. These statistics include word and sentence counts as well as Flesch-Kincaid score. - [Style Guide Compliance API](https://sapling.ai/docs/api/styleguide.md): The styleguide endpoint checks a text for compliance with style rules you define in the request — - [Summarize API](https://sapling.ai/docs/api/summarize.md): The summarize endpoint condenses a longer document — an article, call transcript, meeting notes, - [Superuser Management](https://sapling.ai/docs/api/superuser-management.md): For changing the team directly through API, the key holder must be an superuser for the deployment. - [Team Custom Mappings](https://sapling.ai/docs/api/team-custom-mappings.md): API key holder must be an admin for the team to access a team's dictionary. - [Team Dictionary](https://sapling.ai/docs/api/team-dictionary.md): API key holder must be an admin for the team to access a team's dictionary. - [Team Reporting](https://sapling.ai/docs/api/team-reporting.md): Sapling's API endpoint for exporting team user activity and analytics. See Team Management for getting the IDs for a particular team. - [Team Snippets](https://sapling.ai/docs/api/team-snippets.md): Snippets is a way to save time on frequently used pieces of text. - [User Management](https://sapling.ai/docs/api/team-user-management.md): Teams GET - [Tone Detection](https://sapling.ai/docs/api/tone.md): The tone endpoint checks the tone for a given text. - [Transcriptions](https://sapling.ai/docs/api/transcriptions.md): As part of postprocessing, Sapling offers the ability to process - [Translation API](https://sapling.ai/docs/api/translate.md): The translate endpoint translates text into a target language you name — as an ISO 639 code ### sdk Overview of the Sapling.ai SDK offerings. - [SDK Overview](https://sapling.ai/docs/sdk.md): Overview of the Sapling.ai SDK offerings. - [Deploying to Production](https://sapling.ai/docs/sdk/deploying.md): Here's a sample deployment setup using the JavaScript SDK along with a Python backend. - [Editors Overview](https://sapling.ai/docs/sdk/editors.md): Overview of the Sapling.ai editor integrations. - [CKEditor](https://sapling.ai/docs/sdk/Editors/ckeditor.md): Sample Code - [Froala](https://sapling.ai/docs/sdk/Editors/froala.md): Sample Code - [Quill](https://sapling.ai/docs/sdk/Editors/quill.md): Sample Code - [TinyMCE](https://sapling.ai/docs/sdk/Editors/tinymce.md): Sample Code - [Electron](https://sapling.ai/docs/sdk/electron.md): Electron is a library/framework for building cross-platform desktop apps with JavaScript, HTML, and CSS. - [AI Detect HTML Quickstart](https://sapling.ai/docs/sdk/HTML/ai_detect_quickstart.md): Here's a minimal webpage using the Sapling SDK that includes a button for AI Detection. - [HTML Quickstart](https://sapling.ai/docs/sdk/HTML/quickstart.md): The complete JavaScript API can be found under JavaScript -> Frontend Documentation. - [Accessibility](https://sapling.ai/docs/sdk/Integration Details/accessibility.md): On May 9, 2024, the Department of Health and Human Services - [Style Customization](https://sapling.ai/docs/sdk/Integration Details/customize.md): For technical support, contact support@sapling.ai. - [Flagging Issues in Text](https://sapling.ai/docs/sdk/Integration Details/flagging-issues.md): Sapling's API and SDK can be used in a variety of different ways to process text and automatically flag issues. These can range from grammatical mistakes to making writing more confident, concise, or inclusive. - [Migrating from the Grammarly Text Editor SDK](https://sapling.ai/docs/sdk/Integration Details/grammarly-migration.md): Important: The Grammarly Text Editor SDK will no longer be updated and will be deprecated on January 10, 2024. We suggest removing the integration from your app or website before that date. - Grammarly - [Salesforce Lightning](https://sapling.ai/docs/sdk/Integration Details/lightning.md): For technical support, contact support@sapling.ai. - [AI Detect JavaScript Quickstart](https://sapling.ai/docs/sdk/JavaScript/ai_detect_quickstart.md): Javascript SDK for AI Detection - [Backend Documentation](https://sapling.ai/docs/sdk/JavaScript/backend_documentation.md): Import the Sapling client with: - [Frontend Documentation](https://sapling.ai/docs/sdk/JavaScript/frontend_documentation.md): The Sapling JavaScript SDK enables easy integration of Sapling's functionality -- such as grammar/spell checking and autocomplete -- in any web application. - [Frontend Key Management](https://sapling.ai/docs/sdk/JavaScript/frontend_key_management.md): There are two main ways of authenticating SDK server requests without exposing your API key in public environments: - [JavaScript Quickstart](https://sapling.ai/docs/sdk/JavaScript/quickstart.md): Installation - [Tone Detection JavaScript Quickstart](https://sapling.ai/docs/sdk/JavaScript/tone_quickstart.md): Javascript SDK for Tone Detection - [Overview](https://sapling.ai/docs/sdk/overview.md): Sapling has SDKs that assist in grammar checking text for Python and JavaScript, and an HTTP API that is language agnostic. - [Python Documentation](https://sapling.ai/docs/sdk/Python/documentation.md): The sapling-py package provides a Python wrapper around the Sapling HTTP API. - [Python Quickstart](https://sapling.ai/docs/sdk/Python/quickstart.md): Installation ### onprem - [On-Premises Grammar Checkers and Language Model Assistants](https://sapling.ai/docs/onprem/comparison.md): Looking for a self-hosted grammar/spell check tool? Here's a comparison of 5 different products that offer on-premises deployments, along with the benefits and drawbacks of each product. - [Overview](https://sapling.ai/docs/onprem/overview.md): Self-Hosting ### components - [advanced_edit](https://sapling.ai/docs/components/advanced_edit.md): Options for advanced edits. Option keys are String, and option values are Boolean. - [HTML_custom_UI](https://sapling.ai/docs/components/code_samples/HTML_custom_UI.md): Develop AI-powered messaging applications using Sapling.ai's developer platform. Includes grammar/spelling correction and autocomplete through HTTP API or JavaScript SDK. - [HTML_dev](https://sapling.ai/docs/components/code_samples/HTML_dev.md): Develop AI-powered messaging applications using Sapling.ai's developer platform. Includes grammar/spelling correction and autocomplete through HTTP API or JavaScript SDK. - [HTML](https://sapling.ai/docs/components/code_samples/HTML.md): Develop AI-powered messaging applications using Sapling.ai's developer platform. Includes grammar/spelling correction and autocomplete through HTTP API or JavaScript SDK. - [python_proxy](https://sapling.ai/docs/components/code_samples/python_proxy.md): Develop AI-powered messaging applications using Sapling.ai's developer platform. Includes grammar/spelling correction and autocomplete through HTTP API or JavaScript SDK. - [dev_api_key_tip](https://sapling.ai/docs/components/dev_api_key_tip.md): A rate-limited API developer key can be provisioned from the Sapling API dashboard. Developer keys allow for processing of 50,000 characters every 24 hours. Subscribe for production access and usage-based pricing. - [languages](https://sapling.ai/docs/components/languages.md): Specifies language for grammar and spell checking. - [varieties](https://sapling.ai/docs/components/varieties.md): This option is currently available for: English, Chinese (中文). ### custom-models Language Variety - [Custom Models](https://sapling.ai/docs/custom-models.md): Language Variety ### data Activation - [Data Transfer, Processing, & Retention](https://sapling.ai/docs/data.md): Activation ### help There are several ways to get answers to any questions you have about Sapling. - [Support](https://sapling.ai/docs/help.md): There are several ways to get answers to any questions you have about Sapling. ### integrations With developer platforms, there is often a trade-off - [Other Integrations](https://sapling.ai/docs/integrations.md): With developer platforms, there is often a trade-off ### mcp Connect Sapling's grammar, AI detection, rephrase, summarization, sentiment, language, PII, quality, SEO, classification and extraction tools to an MCP-compatible AI assistant, hosted or local. - [Sapling MCP Server](https://sapling.ai/docs/mcp.md): Connect Sapling's grammar, AI detection, rephrase, summarization, sentiment, language, PII, quality, SEO, classification and extraction tools to an MCP-compatible AI assistant, hosted or local. ### openapi Machine-readable OpenAPI 3.1 description of Sapling's REST API — import it into Postman, generate typed clients, or register Sapling endpoints as tools for ChatGPT Actions and other agent frameworks. - [OpenAPI Specification](https://sapling.ai/docs/openapi.md): Machine-readable OpenAPI 3.1 description of Sapling's REST API — import it into Postman, generate typed clients, or register Sapling endpoints as tools for ChatGPT Actions and other agent frameworks. ### playground We noticed that "playground" is a popular term for AI apps in search traffic analytics. - [Sapling Playground](https://sapling.ai/docs/playground.md): We noticed that "playground" is a popular term for AI apps in search traffic analytics. ### rate_limits Sapling API rate limits, rate-limit headers, the JSON error format, what each HTTP status means, and when to retry. - [Rate Limits & Errors](https://sapling.ai/docs/rate_limits.md): Sapling API rate limits, rate-limit headers, the JSON error format, what each HTTP status means, and when to retry. ### sandbox How to build and test a Sapling API integration safely — the free trial key as a sandbox, zero-cost health checks, which calls change stored state, and moving from test to production. - [Sandbox & Testing](https://sapling.ai/docs/sandbox.md): How to build and test a Sapling API integration safely — the free trial key as a sandbox, zero-cost health checks, which calls change stored state, and moving from test to production. ### uptime You can check on the Sapling API status by checking our status page. - [Status and Uptime](https://sapling.ai/docs/uptime.md): You can check on the Sapling API status by checking our status page. ### usage-pricing Before you subscribe, your API key has a free trial quota of 50,000 characters/day, with 250,000 characters/month. Subscribe for production access and usage-based pricing; subscribed keys default to 10 million characters/day and 25 million characters/month (see Rate Limits). Higher usage volumes are discounted. API subscriptions started on or after August 12, 2026 require a one-time \$5 minimum payment at signup; see Billing for details. - [Usage and Pricing](https://sapling.ai/docs/usage-pricing.md): Before you subscribe, your API key has a free trial quota of 50,000 characters/day, with 250,000 characters/month. Subscribe for production access and usage-based pricing; subscribed keys default to 10 million characters/day and 25 million characters/month (see Rate Limits). Higher usage volumes are discounted. API subscriptions started on or after August 12, 2026 require a one-time \$5 minimum payment at signup; see Billing for details. ## devblog Sapling.ai's developer blog. - [Sapling.ai Dev Blog](https://sapling.ai/devblog.md): Sapling.ai's developer blog. ### archive Archive - [Archive](https://sapling.ai/devblog/archive.md): Archive ### authors Develop AI-powered messaging applications using Sapling.ai's developer platform. Includes grammar/spelling correction and autocomplete through HTTP API or JavaScript SDK. - [Authors](https://sapling.ai/devblog/authors.md): Develop AI-powered messaging applications using Sapling.ai's developer platform. Includes grammar/spelling correction and autocomplete through HTTP API or JavaScript SDK. ### chatgpt-grammar-checking-editing LLMs such as ChatGPT can be used for grammar checking and editing, but there are challenges to be aware of before deploying such systems to production. - [ChatGPT as a Grammar Checker and Editor](https://sapling.ai/devblog/chatgpt-grammar-checking-editing.md): LLMs such as ChatGPT can be used for grammar checking and editing, but there are challenges to be aware of before deploying such systems to production. ### chatgpt-phrases What phrases are used much more frequently by large language models than by people, and what does this reveal about their training process? - [ChatGPT's Favorite Phrases](https://sapling.ai/devblog/chatgpt-phrases.md): What phrases are used much more frequently by large language models than by people, and what does this reveal about their training process? ### tags Develop AI-powered messaging applications using Sapling.ai's developer platform. Includes grammar/spelling correction and autocomplete through HTTP API or JavaScript SDK. - [Tags](https://sapling.ai/devblog/tags.md): Develop AI-powered messaging applications using Sapling.ai's developer platform. Includes grammar/spelling correction and autocomplete through HTTP API or JavaScript SDK. - [2 posts tagged with "llms"](https://sapling.ai/devblog/tags/llms.md): Develop AI-powered messaging applications using Sapling.ai's developer platform. Includes grammar/spelling correction and autocomplete through HTTP API or JavaScript SDK. ## search Develop AI-powered messaging applications using Sapling.ai's developer platform. Includes grammar/spelling correction and autocomplete through HTTP API or JavaScript SDK. - [Search the documentation](https://sapling.ai/search.md): Develop AI-powered messaging applications using Sapling.ai's developer platform. Includes grammar/spelling correction and autocomplete through HTTP API or JavaScript SDK. ## Optional - [Sapling.ai (main site)](https://sapling.ai/): Product overview, live demos, and account signup. - [Sapling MCP Server Guide](https://sapling.ai/docs/mcp/): Install Sapling grammar, AI detection, and language tools in Claude and other MCP-compatible AI assistants. - [Sapling OpenAPI Specification](https://api.sapling.ai/openapi.json): Machine-readable OpenAPI 3.1 description of the REST API, for client generators, Postman, and agent frameworks. No API key needed to fetch it. - [LLM Index](https://sapling.ai/llm/index): Comparison of large language models. - [API Status](https://sapling.statuspage.io/): Live API uptime and incident history. --- # Full Documentation Content ## [ChatGPT as a Grammar Checker and Editor](https://sapling.ai/devblog/chatgpt-grammar-checking-editing.md) November 10, 2024 · 6 min read Ziang Xie CEO, Sapling.ai ## Introduction[​](#introduction "Direct link to Introduction") A popular use case for LLMs is as a writing assistant, for example for editing text as a grammar and spelling checker. Students may need help with writing essays, while businesses use it for drafting and editing emails, marketing copy, reports, etc. An early adopter of language models for writing assistance was [Google](https://arxiv.org/abs/1906.00080) within the Gmail product. With broader availability of LLM platforms for lower costs, major companies have integrated LLMs into their products for writing assistance, such as [Apple](https://www.apple.com/apple-intelligence/) and [Microsoft](https://www.microsoft.com/en-us/microsoft-365/word/ai-writing). However, many of these systems present a popup and do not display suggestions inline. ![Screenshots of writing assistant popups on popular platforms (MacOS, Notion, Gmail)](/img/writing-popups.png) Screenshots of writing assistant popups on popular platforms (MacOS, Notion, Gmail) There has been a gap between the ability of LLMs to generate text and the tight integration of LLMs within text editors. We consider why this is the case, taking the current most popular frontier LLM, ChatGPT-4o mini, as an example, and explain why you should consider using a task-specific LLM for grammar checking (such as the one offered by [Sapling's API](https://sapling.ai/docs/api/edits-overview)). **Tags:** * [llms](https://sapling.ai/devblog/tags/llms.md) [**Read more**](https://sapling.ai/devblog/chatgpt-grammar-checking-editing.md) --- ### 2024[​](#2024 "Direct link to 2024") * [November 10](https://sapling.ai/devblog/chatgpt-grammar-checking-editing.md) [ - ](https://sapling.ai/devblog/chatgpt-grammar-checking-editing.md) [ChatGPT as a Grammar Checker and Editor](https://sapling.ai/devblog/chatgpt-grammar-checking-editing.md) --- # Authors * ## Ziang Xie 2 CEO, Sapling.ai --- # ChatGPT as a Grammar Checker and Editor November 10, 2024 · 6 min read Ziang Xie CEO, Sapling.ai ## Introduction[​](#introduction "Direct link to Introduction") A popular use case for LLMs is as a writing assistant, for example for editing text as a grammar and spelling checker. Students may need help with writing essays, while businesses use it for drafting and editing emails, marketing copy, reports, etc. An early adopter of language models for writing assistance was [Google](https://arxiv.org/abs/1906.00080) within the Gmail product. With broader availability of LLM platforms for lower costs, major companies have integrated LLMs into their products for writing assistance, such as [Apple](https://www.apple.com/apple-intelligence/) and [Microsoft](https://www.microsoft.com/en-us/microsoft-365/word/ai-writing). However, many of these systems present a popup and do not display suggestions inline. ![Screenshots of writing assistant popups on popular platforms (MacOS, Notion, Gmail)](/img/writing-popups.png) Screenshots of writing assistant popups on popular platforms (MacOS, Notion, Gmail) There has been a gap between the ability of LLMs to generate text and the tight integration of LLMs within text editors. We consider why this is the case, taking the current most popular frontier LLM, ChatGPT-4o mini, as an example, and explain why you should consider using a task-specific LLM for grammar checking (such as the one offered by [Sapling's API](https://sapling.ai/docs/api/edits-overview)). ## Limitations of LLMs[​](#limitations-of-llms "Direct link to Limitations of LLMs") While LLMs are now powerful assistants for Q\&A, they often do not generate text in a specific format. For example, OpenAI must enforce that models follow a [Structured Output schema](https://platform.openai.com/docs/guides/structured-outputs) as a separate model capability. For the task of text editing, the simplest output is just the corrected version of the text (the output could also be a list of edits with the edit positions, but this is more expensive in terms of token count, and many LLMs are not great at providing exact numbers without invoking separate modules). During pretraining, LLMs are simply trained to predict the next word in a sequence. In order to perform well at the chat / Q\&A task, LLMs are then instruction fine-tuned so that they respond with a helpful answer instead of just a continuation. To consistently attain an exact output format, either a long prompt or finetuning is needed. Take the following conversations as examples: \[All conversations were with **ChatGPT-4o mini** on `2024-11-14` with no previous context or system prompt.] **Example 1** ``` Human: Correct the grammar: Hi how are you doing Assistant: The corrected sentence is: “Hi, how are you doing?” ``` This results in the **extraneous phrase** "The corrected sentence is:" at the beginning of the response. **Example 2** ``` Human: Correct the grammar: Let me see what we can Assistant: Certainly! A grammatically complete version of the phrase would be: “Let me see what we can do.” This completes the thought and makes the sentence clear and grammatically correct. ``` There is unnecessary text preceding and following the output sentence. The output sentence includes an autocompletion which may or may not be what was desired. **Example 3** ``` Human: Correct the grammar: Hope all's well Assistant: The grammatically correct version would be: “I hope all is well.” ``` The casual statement "Hope all's well" may very well be what the user intended, especially say in a less formal email response. ## Workarounds[​](#workarounds "Direct link to Workarounds") There are two methods to mitigate the above issues: 1. Provide more detailed instructions in the prompt. 2. Finetune the system on the editing task. The first method is easier to get started with, but can be costly as the prompt may often be longer than the input text. ``` Human: You are a grammar assistant for real-time editing. Do not complete partial sentences. Also, do not provide unnecessary edits for casual statements. Only output the corrected text. Now correct the following text: How's it going? Assistant: How's it going? ``` The second method requires some data collection, and also doubles the cost of each API call (as of this writing, for GPT-4o mini, this is `$0.300 / 1M` input tokens and `$0.600 / 1M` output tokens). ## Deployment Considerations[​](#deployment-considerations "Direct link to Deployment Considerations") Outside of the output format, here are some other considerations prior to deploying LLMs for the editing task. ### Benchmarking[​](#benchmarking "Direct link to Benchmarking") We highly recommend benchmarking the performance of the system on a real-world dataset prior to deployment. You can find our guidelines for this [here](https://sapling.ai/docs/api/evaluation/). The intuition is that intentional errors made by developers and administrators doing testing are very different from accidental errors made by users. During testing, false negatives are the emphasis, while during production, false positives get annoying very quickly. ### Latency[​](#latency "Direct link to Latency") While LLMs have become very fast, there is still often a significant delay (time to first token) and processing time (tokens per second). Fortunately, for editing latency is not as critical of a requirement as for autocompletion. Even so, latencies of well under a second are desirable for real-time editing. ### Refusals[​](#refusals "Direct link to Refusals") Surprisingly, sometimes even frontier LLMs will have guardrails in place that prevent them from making recommendations. This can occur, for example, when the text contains offensive language or content. ### Postprocessing[​](#postprocessing "Direct link to Postprocessing") Once you have the corrected text, you'll still need to compute the diff between the old and new text to obtain the edit positions. This is simple for small edits, but for a paragraph where there may be cross-sentence edits, this may not be do-able with a plain edit distance-based method. Sapling's backend handles this for you. ### Customization Options[​](#customization-options "Direct link to Customization Options") Language is not one-size-fits-all. We previously mentioned the example of casual vs. formal English. Another example is that you may need to handle different English varieties (US/UK/AU/CA/etc.). Different businesses may also have their own style guides. Despite learning-based systems greatly outperforming rule-based systems in their ability to generalize, most editing use cases still involve some deterministic logic. ### Displaying Edits[​](#displaying-edits "Direct link to Displaying Edits") Lastly, you'll need to display the edits to the user, which may require significant time from a frontend developer. This is especially true if you wish to display the edits overlaid on top of the editable text. [Sapling's SDK](https://sapling.ai/docs/sdk/overview/) allows you to attach grammar checking functionality to any text input field (`textarea` or `contenteditable`) with a few lines of code. ## Conclusion[​](#conclusion "Direct link to Conclusion") We hope this has been a helpful guide for how you may use ChatGPT as a grammar checker. In case you face some of the challenges presented above, Sapling's API offers a task-specific LLM for grammar checking that may be a good fit. Depending on usage volume, our pricing is competitive with major LLM providers. As of this writing, on our benchmark of 5000 sentences, Sapling's API also significantly outperforms GPT-4o mini on the industry-standard F0.5-score metric (contact us for more details). Contact us at or through our contact form [here](https://www.sapling.ai/contact) for more information. **Tags:** * [llms](https://sapling.ai/devblog/tags/llms.md) --- # ChatGPT's Favorite Phrases October 18, 2023 · 8 min read Ziang Xie CEO, Sapling.ai ![Falling stream of characters.](/img/falling-strings.png) ## Introduction[​](#introduction "Direct link to Introduction") Since the release of OpenAI's ChatGPT in November of 2022, [hundreds of millions](https://www.reuters.com/technology/chatgpt-sets-record-fastest-growing-user-base-analyst-note-2023-02-01/) of users have used it to generate billions of tokens per day. ChatGPT, like other large language models (LLMs), has a distinctive style, and in the initial months of its release would frequently use the phrase "as a large language model" as a precursor to its responses. Since then, this tic appears to have been [reduced](https://www.reddit.com/r/ChatGPT/comments/1576v41/finally_chatgpt_is_no_longer_going_to_say_as_a/), but led us to wonder: what other phrases are strongly associated with ChatGPT's style, but not with the writing of humans? Beyond these statistics, what does this tell us about its training? ## Method[​](#method "Direct link to Method") Our approach was to take question-answer (QA) data with responses by both humans and by ChatGPT. We then determine the phrases that *frequently occur in ChatGPT responses but infrequently occur in human responses*. While this can be done by manually inspecting outputs and making observations, we instead form all possible subsequences of words in the texts (up to length 5) and count the frequency of each. ### Data[​](#data "Direct link to Data") To perform an empirical analysis of ChatGPT's favorite phrases, we used the [HC3 dataset](https://huggingface.co/datasets/Hello-SimpleAI/HC3). This dataset contains \~24K examples of human and AI-generated responses to prompts from Reddit, WikiQA, StackExchange, and other sources of question-answer pairs. We note that this is just one snapshot of responses, and while we may expect the human styles and phrasings to hold steady (at least over the course of a few years), LLMs such as ChatGPT are evolving and changing by the week; the datasets described may need to be updated. ### n-grams[​](#n-grams "Direct link to n-grams") After some simple preprocessing of the data, we computed the 5-grams in each split (human, AI) in the dataset. This resulted in 6,036,620 human n-grams and 3,631,482 ChatGPT n-grams: the first difference we observe is that human-generated text remains more diverse than AI-generated text. We would expect certain [n-grams](https://github.com/orgtre/google-books-ngram-frequency/blob/main/ngrams/5grams_english.csv) to appear frequently in both splits (`at the end of the`, `in whole or in part`) but others to appear much more frequently in human-generated text and others to appear much more frequently in AI-generated text. More precisely perhaps, we would not expect LLMs to generate specific sequences of text due to the methods in which they've been trained and finetuned with human preferences. ## Results[​](#results "Direct link to Results") Based on the analysis above, here are the top 20 phrases most indicative of ***human-generated text***: | Phrase | Log Probability Delta | | --------------------------------- | --------------------- | | the end of the trading | -2.3243 | | put a lot of strain | -2.0574 | | what you want to achieve | -2.0332 | | to give you a basic | -2.0118 | | for the most part its | -2.0069 | | a bit of a mystery | -1.9875 | | i am not a medical | -1.9823 | | is a matter of personal | -1.9750 | | a few of the many | -1.9646 | | i hope that helps let | -1.9407 | | it is up to each | -1.9109 | | the money in the account | -1.9042 | | if you are a nonresident | -1.8966 | | the ratio of the circumference | -1.8936 | | to do this is by | -1.8752 | | are some of the main | -1.8651 | | to the united states constitution | -1.8348 | | the tip of the \[redacted] | -1.8265 | | come up with a plan | -1.7752 | | as a matter of law | -1.7748 | | the best of my ability | -1.7566 | | the length of the side | -1.7447 | | the energy that is released | -1.7409 | | the result of a combination | -1.7016 | | there is also the risk | -1.6976 | Here are the top 20 phrases most likely to appear in ***ChatGPT-generated text*** and not in human-generated text: | Phrase | Log Probability Delta | | ----------------------------------- | --------------------- | | \ there are a couple | 2.9191 | | its also important to think | 2.3774 | | is always a good thing | 2.1334 | | it is also important that | 2.0646 | | it is important to know | 2.0253 | | can vary depending on what | 2.0024 | | i hope this helps \ | 1.9463 | | you may want to check | 1.9204 | | \ another reason is because | 1.9133 | | i hope that helps \ | 1.8907 | | best course of action is | 1.8803 | | it is generally considered impolite | 1.8401 | | it is important to study | 1.8371 | | to keep in mind is | 1.8360 | | i hope this helps you | 1.8343 | | a good idea to go | 1.8296 | | \ there are a lot | 1.8218 | | by a variety of methods | 1.8128 | | \ there are many theories | 1.8114 | | to keep in mind the | 1.8098 | | one way to think of | 1.8061 | | it can be difficult if | 1.7998 | | a good idea to put | 1.7873 | | be able to determine that | 1.7555 | | is important to remember however | 1.7528 | Aside: `` and `` are special tokens indicating the start and end of a text. The number following each n-gram is the difference in log-probability between that text appearing in human vs. ChatGPT-generated text. ## Observations[​](#observations "Direct link to Observations") There are clear patterns in the phrases associated with ChatGPT: * Emphasizing important points (`it is important...`, `is always a good thing`, ...) * Expressing uncertainty (`can vary depending`, `you may want to check`, ...) * Presenting multiple options and viewpoints (`its also important to...`, `it is important to remember...`, ...) Many of these patterns are likely a result of the human examples and preferences that ChatGPT has been tuned on. It's more difficult to immediately identify patterns in the human n-grams. Perhaps a slightly lower degree of formality (`to give you a basic`, `for the most part its`) and also topics such as medical and financial advice where LLMs can be more strict and formulaic given the gravity of the topic. ## Conclusion[​](#conclusion "Direct link to Conclusion") The phrases we've identified demonstrate patterns in ChatGPT's responses that are a reflection of the data that was used to train it. But how strong a signal are they of whether a text was AI-generated or not? AI-generated content is proliferating, and it can be helpful to have surefire "signatures" (such as `as a large language model`) that a text was produced by an LLM. But this is only effective in cases where the user was sloppy and left these telltale signs in the generated text. Many of tried to create **watermarks** that indicate whether an image is AI-generated. These properties can also be put in an image's metadata to indicate its provenance. However, the same is much harder to bake into easily editable text. Some researchers have proposed probabilistic watermarks based on [partitioning vocabularies](https://twitter.com/tomgoldsteincs/status/1618287665006403585); however, these require the participation of all major LLM developers. Interested in other, up-to-date approaches for flagging AI-generated text? Try Sapling's regularly updated [AI detector](https://sapling.ai/ai-content-detector?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs) and [contact us](https://sapling.ai/contact?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs) if you're interested in using it for your business. **Tags:** * [llms](https://sapling.ai/devblog/tags/llms.md) --- ## L[​](#L "Direct link to L") * [llms2](https://sapling.ai/devblog/tags/llms.md) *** --- ## [ChatGPT as a Grammar Checker and Editor](https://sapling.ai/devblog/chatgpt-grammar-checking-editing.md) November 10, 2024 · 6 min read Ziang Xie CEO, Sapling.ai ## Introduction[​](#introduction "Direct link to Introduction") A popular use case for LLMs is as a writing assistant, for example for editing text as a grammar and spelling checker. Students may need help with writing essays, while businesses use it for drafting and editing emails, marketing copy, reports, etc. An early adopter of language models for writing assistance was [Google](https://arxiv.org/abs/1906.00080) within the Gmail product. With broader availability of LLM platforms for lower costs, major companies have integrated LLMs into their products for writing assistance, such as [Apple](https://www.apple.com/apple-intelligence/) and [Microsoft](https://www.microsoft.com/en-us/microsoft-365/word/ai-writing). However, many of these systems present a popup and do not display suggestions inline. ![Screenshots of writing assistant popups on popular platforms (MacOS, Notion, Gmail)](/img/writing-popups.png) Screenshots of writing assistant popups on popular platforms (MacOS, Notion, Gmail) There has been a gap between the ability of LLMs to generate text and the tight integration of LLMs within text editors. We consider why this is the case, taking the current most popular frontier LLM, ChatGPT-4o mini, as an example, and explain why you should consider using a task-specific LLM for grammar checking (such as the one offered by [Sapling's API](https://sapling.ai/docs/api/edits-overview)). **Tags:** * [llms](https://sapling.ai/devblog/tags/llms.md) [**Read more**](https://sapling.ai/devblog/chatgpt-grammar-checking-editing.md) --- [Skip to main content](#__docusaurus_skipToContent_fallback) [![Sapling.ai](/img/logo.svg)![Sapling.ai](/img/logo_dark.svg)](https://sapling.ai/docs.md) [****](https://sapling.ai/docs.md)[Docs](https://sapling.ai/docs.md)[Demo](https://sapling.ai/sdk-demo) [Dev Blog](https://sapling.ai/devblog.md)[Contact](https://sapling.ai/contact)[GitHub](https://github.com/saplingai) Search # Search the documentation Type your search here Powered by[](https://www.algolia.com/) Docs * [API](https://sapling.ai/docs/api/api-access.md) * [SDK](https://sapling.ai/docs/sdk/overview.md) * [LLM Index](https://sapling.ai/llm/index) Community * [Hugging Face](https://huggingface.co/saplingai) * [Discord](https://discord.gg/p5gDc684TX) * [Twitter](https://twitter.com/saplingai) More * [GitHub](https://github.com/saplingai) * [Support](https://sapling.ai/docs/help) Copyright © 2026 Sapling.ai. --- # Welcome [API Overview](https://sapling.ai/docs/api.md)   [SDK Overview](https://sapling.ai/docs/sdk.md) Welcome to the documentation for [Sapling.ai](https://sapling.ai/?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs)'s developer platform. 🚀 Sapling is a platform for building language model applications. Sapling's API provides endpoints for text preprocessing, classification, prediction, correction, and more. Sapling's SDK provides convenient wrappers around the API, as well as UI components for quickly integrating LM-powered functionality in your web application. ## Announcements[​](#announcements "Direct link to Announcements") New: AI detection in Italian and Dutch **October 12, 2026** — The [AI detector API](https://sapling.ai/docs/api/detector.md#languages) now scores Italian and Dutch text with the multilingual model too, and there's an [Italian detector page](https://sapling.ai/it/rilevatore-ia?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs). No request changes are needed. New: AI detection in Spanish, Portuguese, French, German and Japanese **October 9, 2026** — The [AI detector API](https://sapling.ai/docs/api/detector.md#languages) now detects the language of each text and scores Spanish, Portuguese, French, German and Japanese with a dedicated multilingual model. No request changes are needed. ## Getting Started[​](#getting-started "Direct link to Getting Started") Get started by **[following the instructions here](https://sapling.ai/docs/api/api-access.md)**. You can see some of these capabilities by visiting our [Demo](https://sapling.ai/sdk-demo?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs) page. From there, you can start using the Sapling **[HTTP API](https://sapling.ai/docs/api/api-access.md)** or **[SDK](https://sapling.ai/docs/sdk/overview.md)**. To use Sapling's language tools directly from an AI assistant, install the **[Sapling MCP Server](https://sapling.ai/docs/mcp.md)**. You can also try out Sapling using Postman or Zapier. For questions, email us or join our Discord: | Send HTTP requests | Create Zaps | Chat with us | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | | [![Run in Postman](https://run.pstmn.io/button.svg)](https://app.getpostman.com/run-collection/20248607-4e2eafd4-e941-4d52-88a8-b4bb90c1d26a?action=collection%2Ffork\&collection-url=entityId%3D20248607-4e2eafd4-e941-4d52-88a8-b4bb90c1d26a%26entityType%3Dcollection%26workspaceId%3Dcea8d224-5c5e-4604-a8f6-46f228f46f78) | [![Connect in Zapier](/img/zapier.svg)](https://zapier.com/developer/public-invite/153371/ea005ac414d48f5f1323bb424c48cf69) | [![Join Discord](/img/discord.svg)](https://discord.gg/p5gDc684TX) | ## Code Examples[​](#code-examples "Direct link to Code Examples") Find example code in our [GitHub repository](https://github.com/saplingai/sapling-samples), including examples for Python and JavaScript (React, Angular). Start a GitHub issue if your use case isn't handled. ## Use Cases[​](#use-cases "Direct link to Use Cases") Here are some of the things you can do with Sapling: * ⚙️ Integrate deep learning-powered [grammar/spell checking](https://sapling.ai/docs/api/edits-overview.md) into any application, either through the [SDK](https://sapling.ai/docs/sdk/overview.md) or through the [grammar check API](https://sapling.ai/docs/api/edits-overview.md). * ✨ [Autocomplete phrases and sentences](https://sapling.ai/docs/api/autocomplete.md) to save users time typing. * 😀 [Check the tone](https://sapling.ai/docs/api/tone.md) of messages. * [Check the sentiment](https://sapling.ai/docs/api/sentiment.md) of messages. * 🟰 Search for [semantically similar terms](https://sapling.ai/docs/api/similar-terms.md) to augment tagging and search. * 🔍 Pull [semantically relevant responses](https://sapling.ai/docs/api/semsearch.md) for knowledge-base retrieval and search. * 💫 [Rephrase sentences](https://sapling.ai/docs/api/rephrase.md) to adjust formality, length, or syntax. * 🕵🏽‍♀️ [Detect AI-generated content with the AI detector API](https://sapling.ai/docs/api/detector.md) or score the quality of content (contact us for details on quality scoring). You can try out many of the examples on our [utilities page](https://sapling.ai/utilities?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs). New functionality is being released every week. Contact us to learn more or to discuss if Sapling can support your use case. ## Key Principles[​](#key-principles "Direct link to Key Principles") Sapling allows you to easily integrate language model-powered functionality into your application. * **End-to-end**: Sapling is designed to handle the *entire stack*, from preprocessing text inputs, to model inference, to postprocessing outputs, to scoring and filtering outputs. It covers the *last mile* that's required for production systems. * **Predictable**: Sapling is designed to not hallucinate or produce unexpected texts. It returns structured outputs that can reliably be used by production systems. * **Efficient**: Sapling is designed for use in real-time settings. This is true for both Sapling's cloud API as well as its self-hosted offerings. * **Turnkey**: For many applications, Sapling can be integrated with just a few lines of JavaScript/Python code. We ensure that the API/SDK are stable and well-documented (see our [SLA](https://sapling.ai/docs/api/sla.md)). * **Secure**: Whether you use our cloud and require finetuning or you require an airgapped deployment with zero data retention, Sapling can support your data security requirements. ## Subscribing[​](#subscribing "Direct link to Subscribing") Signing up and generating an API key will start you on a free trial of the API. There is a character limit and a 1-month expiration date for the trial. [Subscribe](https://sapling.ai/api-checkout)  to remove quota limits and begin [usage-based pricing](https://sapling.ai/docs/api/pricing) (billed monthly). This will also increase the character limit (up to 10 million characters per day for the `/edits` endpoint, as one example) and upgrade the key so that it does not expire. ## About Us[​](#about-us "Direct link to About Us") Sapling is developed by former engineers and researchers from Berkeley, Google, Meta, and Stanford. ![Cal](https://sapling.ai/static/img/berkeley-logo.svg)![Stanford](https://sapling.ai/static/img/stanford-logo.svg)![Meta](https://sapling.ai/static/img/meta-logo.svg)![Google](https://sapling.ai/static/img/google-logo.svg) --- ## [📄️API Access](https://sapling.ai/docs/api/api-access.md) [Anyone can test out Sapling's API. To get started with the API, simply:](https://sapling.ai/docs/api/api-access.md) --- # Account Status Checks whether an API key can make billable API calls right now. If it can't, the response says why and when the block may clear. Call this endpoint after an unexpected `402`, after a `429` whose `msg` starts with `Rate Limited. Monthly quota used.`, or before a large batch job. It gives a single machine-readable verdict instead of leaving you to work out the account's state from individual error messages. It also works when the key is blocked, because a blocked key is the one that needs this check most. The verdict uses the same payment and quota checks as the billable endpoints, so for **account-wide** blocks it matches what a real request would get: an expired key or subscription, depleted credits, a billing limit, the trial daily allowance or the monthly quota. It does **not** report the short-term, per-endpoint rate limit. If one endpoint answers `429` with `Rate limited. Capacity used.`, this endpoint can still return `api_access: true`, because each endpoint has its own allowance. For that case, use the `429`'s `retry_after` and the `X-RateLimit-*` headers described in [Rate Limits & Errors](https://sapling.ai/docs/rate_limits.md#rate-limit-headers). ## Request Parameters[​](#request-parameters "Direct link to Request Parameters") `https://api.sapling.ai/api/v1/account/status` HTTP method: `GET` or `POST` **key**: String
32-character API key. With `GET`, pass it as a `?key=` query parameter. With `POST`, pass it in the JSON body. With either method you can instead send it as a bearer token in the `Authorization` header. If both are provided, the `key` parameter takes precedence. The endpoint takes no other parameters. Keys only * The public client-side key used by Sapling's website demos gets a `403`. * OAuth access tokens issued for the [MCP server](https://sapling.ai/docs/mcp.md) also get a `403`. Send an API key instead. This call is not billed. Each call counts as 2,000 characters against a separate rate-limit bucket, so polling it doesn't use up your main allowance. Check it when something goes wrong rather than before every request. ## Sample Code[​](#sample-code "Direct link to Sample Code") * cURL * Python * JavaScript ``` curl "https://api.sapling.ai/api/v1/account/status?key=" ``` ``` import requests from pprint import pprint try: response = requests.get( "https://api.sapling.ai/api/v1/account/status", params={"key": ""}, ) if 200 <= response.status_code < 300: pprint(response.json()) else: print('Error: ', str(response.status_code), response.text) except Exception as e: print('Error: ', e) ``` ``` import { Client } from '@saplingai/sapling-js/client'; const client = new Client(''); const { data: status } = await client.accountStatus(); if (!status.api_access) { console.log(`Blocked (${status.blocked_reason}): ${status.message}`); console.log(`Check again in ${status.retry_after} seconds`); } ``` ## Response Parameters[​](#response-parameters "Direct link to Response Parameters") A JSON object. `api_access`, `blocked_reason`, `message` and `usage` are always present. **api\_access**: Boolean
`true` when no account-wide block applies to this key. An individual endpoint can still answer a short-term `429` if its own allowance is used up. **blocked\_reason**: String or null
`null` when `api_access` is `true`. Otherwise it is one of the values below. These values are stable, so you can safely branch on them in code. | `blocked_reason` | Meaning | What a real request returns | Clears when | | ------------------------- | ---------------------------------------------------------------------- | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `key_expired` | The API key has expired. | `402` | You renew the subscription or buy credits. | | `subscription_expired` | The API subscription is no longer active. | `402` | You resubscribe or buy credits. | | `credits_depleted` | The prepaid credit balance is used up. | `402` | You [buy more credits](https://sapling.ai/docs/api/api-access.md#prepaid-api-credits). | | `monthly_billing_limit` | The account reached the monthly billing limit set on its subscription. | `402` | You raise the limit in [API settings](https://sapling.ai/api_settings?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs#billing), or the next billing month starts. | | `trial_daily_limit` | A free-trial key used up its daily character allowance. | `402` | The next UTC midnight. | | `monthly_quota_exhausted` | The key used up its monthly character quota. | `429` | The next UTC month starts. | **message**: String or null
Human-readable explanation of the block, the same text a real request's `msg` would contain. `null` when not blocked. **retry\_after**: Integer
Only present when blocked. The number of seconds until the block may clear, matching the `Retry-After` header a real request would carry: * `trial_daily_limit`: seconds until the next UTC midnight. * `monthly_quota_exhausted`: seconds until the next UTC month starts. * Every other reason: `3600`. That is only a backoff hint. These blocks don't clear on their own, so resolve them in your [API settings](https://sapling.ai/api_settings?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs). **usage**: Object
* **monthly\_quota\_chars** (Integer or null): this key's monthly character quota, or `null` when it has none. * **month\_start** (String (ISO date)): first day of the current UTC month. * **chars\_this\_month** (Integer): characters used by the account since `month_start`. This figure is updated periodically, so it can trail your most recent requests slightly. **subscription**: Object
* **active** (Boolean): whether the account has an API subscription. * **trial\_only** (Boolean): whether the account has only free-trial API access. * **key\_expired** (Boolean): whether this key has passed its expiry date. **credits**: Object
The prepaid credit summary shown in the API dashboard. When `credits.enabled` is `false`, the account has no prepaid credits and only `currency`, `alert_threshold_percent` and `credits_only` are included. `credits_only` is `true` for an account that can be funded only by prepaid credits, so its API access needs a positive balance (otherwise requests get `402` with `credits_depleted`). Sapling can set it automatically after a card payment on your API invoice fails; see [Prepaid API credits](https://sapling.ai/docs/api/api-access.md#prepaid-api-credits). When `enabled` is `true` the object also includes: * `balance_credits`, `purchased_credits`, `consumed_credits` and `consumed_percent` * `low_balance` and `depleted` * `billing_mode` and `stops_when_depleted` * `auto_topup` `low_balance` turns on when you have used `alert_threshold_percent` of the credits bought since the last low-balance warning (or of all credits bought, if there has been no warning yet). It stays on until your next purchase. For example, with an 80% threshold: buy 20 credits and it turns on at 16 used. If you then buy 10 more, it turns off and turns on again at 28 used (the 20 from before the warning, plus 80% of the 10 new credits). `consumed_percent` is always measured against all credits bought. One credit equals one US dollar. **trial**: Object
Only present for free-trial accounts. * **daily\_limit\_chars** (Integer or null): the daily character allowance. * **chars\_used\_today** (Integer): characters used since the last UTC midnight. * **resets\_at** (String): always `utc_midnight`. Team-scoped keys A team-scoped key can be shared by many people, so its response leaves out the personal `subscription`, `credits` and `trial` sections, and `usage.month_start` and `usage.chars_this_month`. You still get the verdict fields (`api_access`, `blocked_reason`, `message`, `retry_after`) and `usage.monthly_quota_chars`. ### Sample Response[​](#sample-response "Direct link to Sample Response") A free-trial key that has used up today's allowance: ``` { "api_access": false, "blocked_reason": "trial_daily_limit", "message": "Free trial daily API character limit reached. Purchase API credits at https://sapling.ai/api_settings#billing to continue, or wait for the limit to reset at UTC midnight.", "retry_after": 22140, "usage": { "monthly_quota_chars": 250000, "month_start": "2026-09-01", "chars_this_month": 131872 }, "subscription": { "active": false, "trial_only": true, "key_expired": false }, "credits": { "enabled": false, "credits_only": false, "currency": "USD", "alert_threshold_percent": 80.0 }, "trial": { "daily_limit_chars": 100000, "chars_used_today": 100412, "resets_at": "utc_midnight" } } ``` A key that is ready to use: ``` { "api_access": true, "blocked_reason": null, "message": null, "usage": { "monthly_quota_chars": 25000000, "month_start": "2026-09-01", "chars_this_month": 1840533 }, "subscription": { "active": true, "trial_only": false, "key_expired": false }, "credits": { "enabled": false, "credits_only": false, "currency": "USD", "alert_threshold_percent": 80.0 } } ``` ## Errors[​](#errors "Direct link to Errors") | Status | When | | ------ | ---------------------------------------------------------------------------------------------- | | `401` | The key is missing or invalid: `{"msg": "Missing API key."}` or `{"msg": "Invalid API key."}`. | | `403` | The public demo key or an MCP OAuth access token was sent. | | `429` | The status bucket is exhausted. Wait `retry_after` seconds. | See [Rate Limits & Errors](https://sapling.ai/docs/rate_limits.md) for the shared error format. --- # Advanced Edits Sapling provides customizable proofreading settings that go beyond regular spelling and grammar checking. These include suggestions around making text simpler and more confident, as well as edits around DEI. The Advanced Edits settings are configurable for your account ([here](https://sapling.ai/language_settings?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs)) as well as on a per API call level (documented below) because these suggestions may not be applicable to all styles of writing. Advanced Edits are currently only available for English. You can find a demo of the sort of suggestions Advanced Edits generates [here](https://sapling.ai/utilities/issue-highlighter?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs). #### Adverbs[​](#adverbs "Direct link to Adverbs") Adverbs can usually be replaced either with a stronger verb. He ran quickly towards the exit. -> He dashed towards the exit. #### Simplifications[​](#simplifications "Direct link to Simplifications") Consider replacing complex phrases with a simpler one with the same meaning. A number of people feel this way. -> Many people feel this way. #### Hard to read[​](#hard-to-read "Direct link to Hard to read") Sentences that are too long can be hard to read. Consider breaking them up for clarity. In the age of digital technology and social media, it's becoming increasingly important for individuals to be aware of their online presence and the impact that their actions and words can have on others. -> In the digital age, it's vital to be aware of how our online presence and actions can impact others. #### Passive Voice[​](#passive-voice "Direct link to Passive Voice") Sentences in the active voice are usually more concise and clear. The ball was kicked by him. -> He kicked the ball. #### Qualifiers[​](#qualifiers "Direct link to Qualifiers") Qualifiers lead to less confident communication. Consider removing them. I was wondering if we can meet tomorrow? -> Can we meet tomorrow? #### DEI[​](#dei "Direct link to DEI") Sapling breaks DEI into the following categories. Example of each are also given: * Gender Pronouns Prefer "they"/"them" over "he"/"him" * Gender Nouns Prefer "chairperson" over "chairman" * Gender Identity Prefer "transgender" over "transexual" * Disability Prefer "autistic" over "aspie" * Age Prefer "person born betwen 1946-1964" over "boomer" * Race Prefer "primary-secondary" over "master-slave" * Social Class Prefer "sex worker" over "prostitute" * Violence Prefer "situation room" over "war room" ## Advanced Edits POST[​](#advanced-edits-post "Direct link to Advanced Edits POST") ### Request Parameters[​](#request-parameters "Direct link to Request Parameters") `https://api.sapling.ai/api/v1/edits` HTTP method: `POST` The Advanced Edits API POST endpoint takes JSON parameters documented below: **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
Text to compute statistics for. **session\_id**: Optional String
Unique name or UUID of document or portion of text that is being processed. If not provided, defaults to current date in `YYYYMMDD` format. **advanced\_edits**: Dictionary
Advanced Edit Options Options for advanced edits. Option keys are `String`, and option values are `Boolean`. * `advanced_edits`: Group setting, covering all the options below * `adverbs` * `simplifications` * `hard_to_read` * `qualifiers` * `voice` * `dei`: Group setting, covering all DEI options below * `gender`: Group setting, covering `gender_pronoun`, `gender_noun`, and `gender_id` * `gender_pronoun` * `gender_noun` * `gender_id` * `sensitivity` * `disability` * `age` * `race` * `social_class` * `violence` A group setting marked `false` will disable all categories it covers. ### Response Parameters[​](#response-parameters "Direct link to Response Parameters") The Edits endpoint with `advanced_edits` specified returns JSON in the format of regular edits [link](https://sapling.ai/docs/api/edits-overview.md#response-parameters). ### Sample Code[​](#sample-code "Direct link to Sample Code") * cURL * JavaScript * Python ``` curl -X POST https://api.sapling.ai/api/v1/edits \ -H "Content-Type: application/json" \ -d '{ "key":"", "text":"The ball was kicked. Suddenly there was the sound of an aircraft.", "session_id": "test session", "advanced_edits": { "advanced_edits": true } }' ``` ``` import axios from 'axios'; async function run(text) { try { const response = await axios.post( 'https://api.sapling.ai/api/v1/edits', { key: '', session_id: 'test session', text, advanced_edits: { advanced_edits: true, }, }, ); const {status, data} = response; console.log({status}); console.log(JSON.stringify(data, null, 4)); } catch (err) { const { msg } = err.response.data; console.log({err: msg}); } } run('The ball was kicked. Suddenly there was the sound of an aircraft.'); ``` ``` import requests from pprint import pprint key = '' url = 'https://api.sapling.ai/api/v1/edits' data = { 'key': key, 'text': 'The ball was kicked. Suddenly there was the sound of an aircraft.', 'session_id': 'Test Document UUID', 'advanced_edits': { 'advanced_edits': True, }, } try: response = requests.post(url, json=data) if 200 <= response.status_code < 300: resp_json = response.json() edits = resp_json['edits'] pprint(edits) else: print('Error: ', str(response.status_code), response.text) except Exception as e: print('Error: ', e) ``` ### Sample Response[​](#sample-response "Direct link to Sample Response") ``` [ { "description": "Reduce the use of passive voice.", "end": 19, "error_type": "voice", "general_error_type": "Advanced Edit", "id": "81695ccc-2916-50ba-8b93-c3ed7bb55f54", "replacement": "", "sentence": "The ball was kicked.", "sentence_start": 0, "start": 9 }, { "description": "Reduce the use of adverbs.", "end": 8, "error_type": "adverbs", "general_error_type": "Advanced Edit", "id": "62cb63df-7c3d-52de-aab2-eded6924d689", "replacement": "", "sentence": "Suddenly there was the sound of an aircraft.", "sentence_start": 21, "start": 0 }, { "description": "Use simpler language where possible.", "end": 43, "error_type": "simplifications", "general_error_type": "Advanced Edit", "id": "a0367693-de21-524f-9a3e-abe7a62b46f6", "replacement": "plane", "sentence": "Suddenly there was the sound of an aircraft.", "sentence_start": 21, "start": 35 } ] ``` --- # Analysis The Analysis endpoints compute syntactic information about the input text. Outputs include: * Adverbs and other parts of speech of tokens; * Whether input sentences are in passive or active voice; * Whether input sentences are hard to read (`hard`) or very hard to read (`very_hard`); * Phrases that can be simplified (e.g., `utilize` -> `use`); ## Analysis POST[​](#analysis-post "Direct link to Analysis POST") ### Request Parameters[​](#request-parameters "Direct link to Request Parameters") `https://api.sapling.ai/api/v1/analysis_h1` HTTP method: `POST` The `analysis_h1` API POST endpoint takes JSON parameters documented below: **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
Text to compute analysis for. **session\_id**: Optional String
Unique name or UUID of document or portion of text that is being processed. If not provided, defaults to current date in `YYYYMMDD` format. ### Response Parameters[​](#response-parameters "Direct link to Response Parameters") The `analysis_h1` POST endpoint returns JSON of the following format: ``` { "adverbs": [ // Adverbs with word indices ["really", 9], ["so", 17], ... ], "hard_to_read": [ { "difficulty": "hard", "sentence": "A long sentence that is hard to read..." }, ... ], "simplifications": [ { "start": 373, // Character index "end": 382, "original": "utilize", "replacements": [ "use" ] }, ... ], "voice": [ { "sentence": "The ball was caught", "voice": "passive" }, ... ] } ```   --- # API Access Anyone can test out Sapling's API. To get started with the API, simply: 1. Register for a Sapling account [here](https://sapling.ai/user/register?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs). 2. Once you've registered and signed in, visit your Sapling [API settings dashboard](https://sapling.ai/api_settings?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs). 3. Click the Generate Key button under the "API" section of the settings pane. 4. Start using the API by following the API documentation. We recommend starting with the [Grammar Check API (Edits)](https://sapling.ai/docs/api/edits-overview.md) guide. The key starts a free trial that works as a sandbox: it is never charged, and almost every endpoint only reads the text you send. See [Sandbox & Testing](https://sapling.ai/docs/sandbox.md) for the trial limits, zero-cost health checks, and the few calls that change stored state. When making requests, you can authenticate by including your API key as a `key` parameter or by providing it as a bearer token in the `Authorization` header. If both are supplied, the `key` parameter takes precedence. A machine-readable [OpenAPI 3.1 specification](https://sapling.ai/docs/openapi.md) of the API is available at `https://api.sapling.ai/openapi.json`. Use it to import the API into Postman, generate a typed client in any language, or register Sapling endpoints as tools for an AI agent. ## Prepaid API credits[​](#prepaid-api-credits "Direct link to Prepaid API credits") You can purchase prepaid API credits from the **Billing** section of the [API settings dashboard](https://sapling.ai/api_settings?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs#billing). One credit equals one US dollar, and API usage draws down the balance using the same per-character prices described on the [API pricing](https://sapling.ai/docs/api/pricing) page. The dashboard shows purchased, consumed, and remaining credits. Sapling sends a low-balance warning once you have used the configured share (shown as `alert_threshold_percent` in [Account Status](https://sapling.ai/docs/api/account-status.md)) of the credits bought since the last warning, so a top-up after a warning starts a fresh count. A positive prepaid balance can activate or reactivate API access without a metered subscription; when the balance is depleted, API requests return `402 Payment Required` until more credits are purchased. If a card payment on your API invoice fails, Sapling can switch the account to prepaid credits only. [Account Status](https://sapling.ai/docs/api/account-status.md) then reports `credits_only: true`, and API requests are served only while you have a positive prepaid balance. Buy credits to keep using the API, and [contact us](https://sapling.ai/contact?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs) once your card is fixed to go back to monthly billing. ## Payment-required responses[​](#payment-required-responses "Direct link to Payment-required responses") Authenticated API requests can return `402 Payment Required` when the API key has expired, the associated subscription is inactive, the account has reached its monthly billing limit, or its prepaid credit balance is depleted. These responses include a `Retry-After: 3600` header, where the value is the number of seconds clients should wait before trying again. The header is listed in `Access-Control-Expose-Headers`, so cross-origin browser clients can read it. Because resolving a `402` usually requires renewing access or updating billing settings, clients should inspect the response's `msg` field and avoid retrying continuously while the account remains inactive. One `402` is temporary. When a free-trial account uses up its daily character allowance, the block clears at the next UTC midnight. That response's `Retry-After` header and a numeric `retry_after` body field both give the seconds until the reset. To check a key's state without making a billable call, including the exact reason it is blocked, use [Account Status](https://sapling.ai/docs/api/account-status.md). See [Rate Limits & Errors](https://sapling.ai/docs/rate_limits.md) for every status code. The response body is JSON with a `msg` field describing the reason: ``` { "msg": "API credit balance depleted. Purchase more credits at https://sapling.ai/api_settings#billing." } ```
Production Key Trial development keys allow for processing of 50,000 characters every 24 hours. [Subscribe](https://sapling.ai/api-checkout) for production access and [usage-based pricing](https://sapling.ai/docs/api/pricing), or purchase [prepaid API credits](#prepaid-api-credits) to activate production access without a metered subscription. --- # API Reporting Sapling's API endpoints for retrieving API usage analytics, plus the per-request `return_usage` flag for seeing a request's own usage inline. ## Per-Request Usage[​](#per-request-usage "Direct link to Per-Request Usage") Every API-key-authenticated endpoint accepts an opt-in `return_usage` flag. When set, a successful JSON response additionally reports the usage this request submitted for metering, as a top-level `usage` field: ``` { "usage": {"characters": 1000} } ``` **return\_usage**: Optional Boolean
Pass `true` in the JSON request body. For GET and multipart (file upload) requests, pass it as a `?return_usage=true` query or form parameter — on the PDF/DOCX file endpoints it can also ride in the `jsonParams` part. Defaults to `false`. ``` curl -X POST https://api.sapling.ai/api/v1/classify \ -H "Content-Type: application/json" \ -d '{"key":"", "text":"I was charged twice.", "labels":["billing", "other"], "return_usage": true}' ``` `characters` is the count the request submitted for metering, before repeat-request deduplication (see below). It may therefore exceed what this request adds to your bill; use the [reporting endpoints](#api-usage-post) or your receipt for billed totals. It is counted per sentence for `/edits` and `/spellcheck`, once per **distinct** item of a batch `texts` request, and honors endpoint-specific metering (for example, `/seo` meters the page text). The 2.5× multiplier for Chinese, Japanese and Korean text is included, so CJK requests report more than their raw length. Requests that don't bill report `{"characters": 0}`. A few things to know: * The count is captured in the request itself, **before** the billing pipeline's repeat-request handling applies (see [Characters and Repeat Requests](https://sapling.ai/docs/api/pricing#characters-and-repeat-requests)), so it is an upper bound on what the request finally adds to your bill: text that was already billed recently is counted here but not re-charged. * `usage` is attached only to successful (2xx) JSON object responses. Error responses, responses whose body is not a JSON object, streamed responses, and responses that already carry a `usage` key (such as [`/account/status`](https://sapling.ai/docs/api/account-status.md) and the LLM completion endpoints' token usage) are returned unchanged. * The flag is advertised on every API-key-authenticated operation in [`GET /api/v1/openapi.json`](https://sapling.ai/docs/openapi.md), so generated SDKs and agents pick it up automatically. ## API Usage POST[​](#api-usage-post "Direct link to API Usage POST") A self-serve endpoint for retrieving aggregate API usage, cost, and quota information for the account that owns the calling API key. Usage is scoped to the account that owns the calling key: * For a **team-scoped** key, the response covers all of the team's active keys. * For a **user** key, the response covers all of that user's active keys. The endpoint returns **aggregates only**. It does not return per-call rows and does not support pagination or `limit`/`offset` parameters. No request text or content hashes are stored in, or returned by, this endpoint. Masked key IDs API keys are never returned in raw form. In every section of the response, each key is identified by a stable, masked ID of the form `****-`, where `` is the last four characters of the key and `` is the first eight hex characters of the SHA-256 hash of the key. The same key always maps to the same masked ID, and the ID is consistent across every section of the response (`by_api_key`, `daily_usage`, and `monthly_usage`). ### Request Parameters[​](#request-parameters "Direct link to Request Parameters") `https://api.sapling.ai/api/v1/reporting/api_usage` HTTP method: `POST` **key**: String
32-character API key. Can also be supplied as a `?key=` query parameter or via the `Authorization` header as a bearer token. If multiple are provided, the `key` parameter takes precedence. **start\_days\_back**: Optional Integer
Number of days back at which the reporting window starts (e.g. `7` starts the window one week ago). Defaults to the start of the current calendar month. **end\_days\_back**: Optional Integer
Number of days back at which the reporting window ends. Defaults to `0` (today, inclusive). Both `start_days_back` and `end_days_back` are validated as follows: * Each must be an integer, otherwise the endpoint returns HTTP `400`. * Each must be non-negative, otherwise the endpoint returns HTTP `400`. * `start_days_back` must be greater than or equal to `end_days_back`, otherwise the endpoint returns HTTP `400`. * The resolved window may not exceed 1 year, otherwise the endpoint returns HTTP `403`. The window is also clamped to the age of the account, so it will never start before the account was created. ### Sample Code[​](#sample-code "Direct link to Sample Code") * cURL * Python * JavaScript ``` curl -X POST https://api.sapling.ai/api/v1/reporting/api_usage \ -H "Content-Type: application/json" \ -d '{"key":"", "start_days_back": 30, "end_days_back": 0}' ``` ``` import requests from pprint import pprint try: response = requests.post( "https://api.sapling.ai/api/v1/reporting/api_usage", json={ "key": "", "start_days_back": 30, "end_days_back": 0, } ) if 200 <= response.status_code < 300: pprint(response.json()) else: print('Error: ', str(response.status_code), response.text) except Exception as e: print('Error: ', e) ``` ``` import { Client } from '@saplingai/sapling-js/client'; const client = new Client(''); const { data: usage } = await client.apiUsage({ start_days_back: 30, end_days_back: 0 }); console.log(usage.total_characters, usage.total_cost_usd, usage.by_endpoint); ``` The JavaScript client (`@saplingai/sapling-js` 1.0.54 and later) also has `apiQuotaUsage()`, `apiEndpointUsage()` and `apiUserActivity()` for the endpoints below. See [Usage and quota reporting](https://sapling.ai/docs/sdk/JavaScript/backend_documentation.md#usage-and-quota-reporting). ### Response Parameters[​](#response-parameters "Direct link to Response Parameters") JSON structure with aggregate usage, cost, and quota information for the resolved window. **start\_date** / **end\_date**: String (ISO date)
Inclusive start and end dates of the resolved reporting window. **total\_characters**: Integer
Total characters processed across the account during the window. **total\_cost\_usd**: Float
Cost of the usage in the window at list pricing, computed per (endpoint, month). See the [Pricing](https://sapling.ai/docs/api/pricing) page for list rates. **currency**: String
Currency of `total_cost_usd` and `by_endpoint[*].cost_usd` (e.g. `USD`). **by\_api\_key**: Object
Map keyed by masked key ID. Each value has the following fields: * **api\_key** (String): The masked key ID, e.g. `****abcd-1a2b3c4d`. * **tag** (String): The key's tag/label, if any. * **characters** (Integer): Characters processed by this key in the window. * **monthly\_limit** (Integer): Monthly character limit (`-1` means no limit). * **monthly\_usage** (Integer): Characters used in the current calendar month. * **remaining\_quota** (Integer or null): Remaining characters this month (`null` when there is no limit). * **quota\_exceeded** (Boolean): Whether the monthly limit has been exceeded. **daily\_usage**: Object
Map of masked key ID to a `{ "YYYY-MM-DD": }` object giving characters processed per day. **monthly\_usage**: Object
Map of masked key ID to a `{ "YYYY-MM": }` object giving characters processed per calendar month. **by\_endpoint**: Object
Map of endpoint name (e.g. `EDIT`) to `{ "characters": , "cost_usd": }`. **last\_updated**: String (ISO timestamp) or null
Timestamp of the most recent usage record in the window, or `null` if there is no usage in the window. **credit\_balance\_data**: Object
Your prepaid credit balance: the same object as `credits` in [Account Status](https://sapling.ai/docs/api/account-status.md). It is omitted for team-scoped keys, so a key shared across a team doesn't expose one person's balance. #### Sample Response[​](#sample-response "Direct link to Sample Response") ``` { "start_date": "2026-05-13", "end_date": "2026-06-12", "total_characters": 36000, "total_cost_usd": 0.9, "currency": "USD", "by_api_key": { "****abcd-1a2b3c4d": { "api_key": "****abcd-1a2b3c4d", "tag": "production", "characters": 20000, "monthly_limit": 1000000, "monthly_usage": 20000, "remaining_quota": 980000, "quota_exceeded": false }, "****wxyz-9f8e7d6c": { "api_key": "****wxyz-9f8e7d6c", "tag": "staging", "characters": 16000, "monthly_limit": -1, "monthly_usage": 16000, "remaining_quota": null, "quota_exceeded": false } }, "daily_usage": { "****abcd-1a2b3c4d": { "2026-06-10": 4000, "2026-06-11": 16000 }, "****wxyz-9f8e7d6c": { "2026-06-11": 16000 } }, "monthly_usage": { "****abcd-1a2b3c4d": { "2026-05": 0, "2026-06": 20000 }, "****wxyz-9f8e7d6c": { "2026-06": 16000 } }, "by_endpoint": { "EDIT": { "characters": 20000, "cost_usd": 0.5 }, "SPELLCHECK": { "characters": 16000, "cost_usd": 0.4 } }, "last_updated": "2026-06-11T17:06:15.416190", "credit_balance_data": { "enabled": false, "credits_only": false, "currency": "USD", "alert_threshold_percent": 80.0 } } ``` ## API Quota Usage POST[​](#api-quota-usage-post "Direct link to API Quota Usage POST") This endpoint allows users to get API usage for a given time period as well as check their current quota. ### Request Parameters[​](#request-parameters-1 "Direct link to Request Parameters") `https://api.sapling.ai/api/v1/reporting/api_quota_usage` HTTP method: `POST` **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. **start\_days\_back**: Optional Integer
Starting number of days ago (7 would consider usage starting 1 week ago). Defaults to `1`; `0` is also read as `1`, so the shortest window is yesterday and today. **end\_days\_back**: Optional Integer
Ending number of days ago (0 would mean up until current date). Defaults to `0`. ### Response Parameters[​](#response-parameters-1 "Direct link to Response Parameters") JSON structure with API usage reporting information. There may be additional metadata that is subject to change. ``` { "quota": , // Monthly character limit of the API key (-1 when there is none) "usage": , // Usage volume during time period "accepts": , // Suggestions accepted via the accept endpoint since the 1st of this month "ignores": , // Suggestions ignored via the reject endpoint since the 1st of this month "last_updated": , // Timestamp that usage information was last updated (Unix epoch seconds) } ``` `accepts` and `ignores` always cover the current calendar month, whatever window you request. ## API Endpoint Usage[​](#api-endpoint-usage "Direct link to API Endpoint Usage") This endpoint allows users to get API usage for each endpoint over a given time period. ### Request Parameters[​](#request-parameters-2 "Direct link to Request Parameters") `https://api.sapling.ai/api/v1/reporting/api_endpoint_usage` HTTP method: `POST` **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. **start\_days\_back**: Optional Integer
Starting number of days ago (7 would consider usage starting 1 week ago). Defaults to `6`. **end\_days\_back**: Optional Integer
Ending number of days ago (0 would mean up until current date). Defaults to `0`. **interval**: Optional String
Interval to report usage by. Can be `Daily`, `Weekly`, or `Monthly`. Defaults to `Daily`. ### Response Parameters[​](#response-parameters-2 "Direct link to Response Parameters") JSON structure with API endpoint usage reporting information. For each endpoint, e.g. `EDIT` or `SPELLCHECK`, the usage is reported for each interval range in the time period. ``` { "data": { "": { "": , ... } }, "last_updated": "" } ``` Example: ``` { "data": { "EDIT": { "0": 0, "1": 16000, "2": 0, "3": 0, }, "SPELLCHECK": { "0": 0, "1": 0, "2": 20000, "3": 0, } }, "last_updated": "2025-03-14T17:06:15.416190" } ``` ## API User Activity[​](#api-user-activity "Direct link to API User Activity") This endpoint allows users to get recorded user actions over a given time period. The current actions recorded are: * Edits shown * Edits accepted * Edits ignored Please note these require that the `accept` and `reject` API endpoints have been used. The endpoint also returns active users. ### Request Parameters[​](#request-parameters-3 "Direct link to Request Parameters") `https://api.sapling.ai/api/v1/reporting/api_user_activity` HTTP method: `POST` **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. **start\_days\_back**: Optional Integer
Starting number of days ago (7 would consider usage starting 1 week ago). Defaults to `6`. **end\_days\_back**: Optional Integer
Ending number of days ago (0 would mean up until current date). Defaults to `0`. ### Response Parameters[​](#response-parameters-3 "Direct link to Response Parameters") JSON structure with activity reporting information. ``` { "edits_shown": , "edits_accepted": , "edits_ignored": , "active_users": { "logged_in": , "anonymous": } } ``` --- # Applying Edits After you've sent the Sapling API text and gotten JSON edits in the response, how do you apply the edits to get updated text? The simplest way is to use the `auto_apply` argument. By setting this to true, the returned response will have an extra field, `applied_text`, that contains the text with the edits applied. However, you can also easily apply edits programmatically. ## Programmatically Applying Edits[​](#programmatically-applying-edits "Direct link to Programmatically Applying Edits") Recall the Edit data structure: ``` { "id": , // Opaque edit id, used to give feedback "sentence": , // Unedited sentence "sentence_start": , // Offset of sentence from start of text "start": , // Offset of edit start relative to sentence "end": , // Offset of edit end relative to sentence "replacement": , // Suggested replacement "error_type": , // Error type, see "Error Categories" "general_error_type": , // See "Error Categories" } ``` When programmatically applying edits, go in *reverse* start offset (`sentence_start + start`) order so changes don't affect the offsets of the remaining edits. For example, consider the sentence: `Lets go to the housee.` where Sapling returns the following list of edits: ``` [ { 'sentence_start': 0, 'start': 0, 'end': 4, 'replacement': "Let's", ... }, { 'sentence_start': 0, 'start': 15, 'end': 21, 'replacement': 'house', ... } ] ``` The simplest way to apply the edits to your text is in reverse order: 1. Replace characters `15-21` with `house`. 2. Replace characters `0-4` with `Let's`. If the characters for `Lets` are replaced before `housee`, the offsets for other edits would need to be updated. ## Sample Code[​](#sample-code "Direct link to Sample Code") We provide sample code below for applying edits. A few things to keep in mind: * The `edits` array is ordered by starting position, though we include logic below to ensure this is the case. * For some languages where assignment is by reference, you will want to create a copy of the original string before modifying it. - Python - JavaScript - PHP ``` text = str(text) edits = sorted(edits, key=lambda e: (e['sentence_start'] + e['start']), reverse=True) for edit in edits: start = edit['sentence_start'] + edit['start'] end = edit['sentence_start'] + edit['end'] if start > len(text) or end > len(text): print(f'Edit start:{start}/end:{end} outside of bounds of text:{text}') continue text = text[: start] + edit['replacement'] + text[end:] return text ``` ``` function apply_edits(text, edits) { text = text.slice(); const reversed = edits.sort((a, b) => b['sentence_start'] + b['start'] - a['sentence_start'] - a['start']); for (const edit of reversed) { const start = edit['sentence_start'] + edit['start']; const end = edit['sentence_start'] + edit['end']; if (start > text.length || end > text.length) { console.log(`Edit start:${start}/end:${end} outside of bounds of text:${text}`); continue; } text = text.slice(0, start) + edit['replacement'] + text.slice(end); } return text; } ``` ``` function cmp($a, $b) { return $b->sentence_start + $b->start - $a->sentence_start - $a->start; } function apply_edits($text, $edits) { usort($edits, 'cmp'); foreach($edits as $edit) { $start = $edit->sentence_start + $edit->start; $end = $edit->sentence_start + $edit->end; if ($start > strlen($text) or $end > strlen($text)) { echo 'Index out of range'; continue; } $text = substr($text, 0, $start) + $edit->replacement + substr($text, $end, null); } return $text; } ``` --- # Autocomplete Sapling's autocomplete provides predictions of the next few characters or words given the current context in a particular editable. The predictions are based on the previous text. Example: a possible completion for the context, `Let us know if `  might be `you need anything else`. [Try it out](https://sapling.ai/sdk-demo) You can easily integrate the autocomplete UI and functionality into your application using the [SDK](https://sapling.ai/docs/sdk.md). ## Request Completion POST[​](#request-completion-post "Direct link to Request Completion POST") ### Sample Code[​](#sample-code "Direct link to Sample Code") * cURL * JavaScript * Python ``` curl -X POST https://api.sapling.ai/api/v1/complete \ -H "Content-Type: application/json" \ -d '{ "key":"", "query":"Hi how are", "session_id": "test session", }' ``` ``` import axios from 'axios'; async function run(query) { try { const response = await axios.post( 'https://api.sapling.ai/api/v1/complete', { key: '', session_id: 'test session', query, }, ); const {status, data} = response; console.log({status}); console.log(JSON.stringify(data, null, 4)); } catch (err) { const { msg } = err.response.data; console.log({err: msg}); } } run('Hi how are'); ``` ``` import requests from pprint import pprint key = '' url = 'https://api.sapling.ai/api/v1/complete' data = { 'key': key, 'query': 'Hi how are', 'session_id': 'test session', } try: response = requests.post(url, json=data) if 200 <= response.status_code < 300: resp_json = response.json() edits = resp_json['edits'] pprint(edits) else: print('Error: ', str(response.status_code), response.text) except Exception as e: print('Error: ', e) ``` ### Sample Response[​](#sample-response "Direct link to Sample Response") ``` { "hash":"e3fffa5f-1f11-531e-8a8e-d9e758677e0c", "output":" you", "query":"Hi how are" } ``` ### Request Parameters[​](#request-parameters "Direct link to Request Parameters") `https://api.sapling.ai/api/v1/complete` HTTP method: `POST` JSON parameters: **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. **session\_id**: Optional String
Unique name or UUID of document or portion of text that is being checked. If not provided, defaults to current date in `YYYYMMDD` format. **query**: String
Text that should be autocompleted. ### Response Parameters[​](#response-parameters "Direct link to Response Parameters") The Autocomplete endpoint returns JSON with parameters documented below: **output**: String
Predicted completion. For example, for the query `Hi, how are`, the predicted completion should be `you` (with a leading space). **query**: String
Actual query used to generate completion (may be a truncated version of request query). **hash**: String (UUID)
Opaque ID of completion for use with accept event. ## Accept Completion POST[​](#accept-completion-post "Direct link to Accept Completion POST") Use this API endpoint to have Sapling adapt its system over time. Each suggested completion has an completion UUID. You can pass this information back to Sapling to indicate the completion was used. For each unique completion UUID, use the accept endpoint up to once. ### Request Parameters[​](#request-parameters-1 "Direct link to Request Parameters") `https://api.sapling.ai/api/v1/complete//accept` HTTP method: `POST` **\**: String (UUID)
ID of completion returned from a Completion POST. Here's a sample request body: ``` { "key": "", "context": { "query": "Hi how are", "completion": " you" }, "session_id": "Test Document UUID" } ``` Note that the `query` and `completion` arguments are required. ### Response Parameters[​](#response-parameters-1 "Direct link to Response Parameters") Will return error or `{"result": "success"}`. --- # Chunking Also refered to as **segmentation**, **chunking** text allows you to parallelize the processing of a piece of text or reduce it into pieces that fall under the length limit of a particular system (e.g., under the context length of a Large Language Model). The first step is to identify delimiters where the text can be chunked. For many use cases, this might be a double newline indicating the start of a new paragraph. In other instances, you may want chunks of roughly equal length; there you can instead chunk on the closest ending punctuation marker (e.g., `.`, `?`, `!`). If latency is critical for your use of Sapling, you'll definitely want to chunk longer documents into much shorter texts so that you can process them in parallel. Sapling also provides certain endpoints such as the AI detection endpoint that have a length limit though you may want to feed a piece of text with length exceeding that limit. What follows are some notes on how we would approach chunking, but contact us if you need help doing this. Levels: * At the highest level, you have a **corpus** of documents. You may have heard of Common Crawl as one such corpus of documents across the web, but there are also corpora based on just Wikipedia, or Reddit, or internal documents belonging to a corporation. * Beneath the corpus level, we have **documents**. Documents represent a cohesive piece of text and are a familiar concept--think Microsoft Word or Google Docs. For news or magazine publications, **article** may be a more accurate term than **document**. In some cases, it may be unclear if text should be grouped into one or many documents--e.g., in the case of book chapters--but regardless, in these cases it's straightforward to segment the text in either fashion. * Within a document, there is a greater variety of segments that may work best. We present one that's drawn from the HTML DOM standard: a document is composed of **sections**, where each section contains **headings** and **paragraphs**. The paragraphs are further grouped into **sentences**, which consist of **tokens**. Tokens can be words, or subwords/morphemes, or characters, or even longer text segments when lexicons are built using information-theoretic approaches. Hence we arrive at a structure that looks like the following (in JSON format): ``` { "corpus": [ { "title": "Test Document 1", "body": [ { "title": "Section 1", "body": [ { "heading": "Heading 1" }, { "paragraph": "Paragraph text" }, { "paragraph": "More paragraph text" } ] } ] } ] } ``` To help with the tasks above, Sapling offers endpoints for performing simple length-based chunking. Given an input text or HTML document, Sapling will break the text into blocks of length of most `max_length`. When splitting the text, the API follows the following preference stack: ``` page break > paragraph breaks > line breaks > tabs > punctuation > all other whitespace ``` Chunks don't overlap, and each delimiter stays at the end of the chunk before the split, so joining the chunks gives back the original text. If no preferred split point is found, the text is cut at exactly `max_length` characters. tip This endpoint is free of charge. ## Chunk Text POST[​](#chunk-text-post "Direct link to Chunk Text POST") ### Sample Code[​](#sample-code "Direct link to Sample Code") * cURL * Python ``` curl -X POST https://api.sapling.ai/api/v1/ingest/chunk_text \ -H "Content-Type: application/json" \ -d '{"key": "", "text": "Here is one paragraph.\n\nHere is another.", "max_length": 25}' ``` info If you want to pretty-print JSON output so it's easier to read and you have Python installed, you can pipe command line outputs into `| python -m json.tool`. Example: `echo "{\"text\": \"Some text\"}" | python -m json.tool` ``` import requests from pprint import pprint try: response = requests.post( "https://api.sapling.ai/api/v1/ingest/chunk_text", json={ "key": "", "text": "Here is one paragraph.\n\nHere is another.", "max_length": 25 } ) if 200 <= response.status_code < 300: resp_json = response.json() chunks = resp_json['chunks'] pprint(chunks) else: print("Error: ", str(response.status_code), response.text) except Exception as e: print("Error: ", e) ``` ### Request Parameters[​](#request-parameters "Direct link to Request Parameters") `https://api.sapling.ai/api/v1/ingest/chunk_text` HTTP method: `POST` **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
Text to chunk. **max\_length**: Integer
Maximum length of each chunk, in characters. Required: it must be a positive JSON integer, so a quoted number such as `"25"` returns a `400`. **step\_size**: Optional Integer
Size of window to look for split points. The larger the step size, the greater the variance in chunk sizes, but the higher the chance of splitting on a preferred split type (see preference stack above). Defaults to 10% of the text length. ### Response Parameters[​](#response-parameters "Direct link to Response Parameters") JSON with the field `chunks` containing a list of strings will be returned, representing the segmented text. For the sample request above: ``` { "chunks": [ "Here is one paragraph.\n\n", "Here is another." ] } ``` ## Chunk HTML POST[​](#chunk-html-post "Direct link to Chunk HTML POST") ### Request Parameters[​](#request-parameters-1 "Direct link to Request Parameters") `https://api.sapling.ai/api/v1/ingest/chunk_html` HTTP method: `POST` **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. **html**: String
HTML to extract text from and chunk. **max\_length**: Integer
Maximum length of each chunk, in characters. Required: it must be a positive JSON integer, so a quoted number such as `"25"` returns a `400`. **step\_size**: Optional Integer
Size of window to look for split points. The larger the step size, the greater the variance in chunk sizes, but the higher the chance of splitting on a preferred split type (see preference stack above). Defaults to 10% of the text length. ### Response Parameters[​](#response-parameters-1 "Direct link to Response Parameters") info This endpoint not only breaks up the HTML but also discards all HTML tags, resulting in plain text. Some HTML fields that you wish to keep may also be discarded; contact us if this is the case so we can better support your use case. JSON with the field `chunks` containing a list of strings will be returned, representing the segmented text contained within the HTML. ``` { "chunks": [ "First chunk", "Second chunk" ] } ``` --- # Text Classification API The classify endpoint sorts a piece of text into **labels you define** in the request — no training data, no per-customer model. Send the text together with 2–20 label names (optionally with a short description of each), and the endpoint returns the best-matching label, a score for every label, and a one-sentence rationale pointing at the evidence in the text. Two modes are supported: * **Single-label** (the default): exactly one label applies. The scores form a probability distribution over your labels (they sum to \~1) and `label` is the top one. * **Multi-label** (`multi_label: true`): zero, one or several labels may apply at once. Each label gets an independent 0–1 score, and `labels` lists every label at or above a `threshold` you control. It is built for support-ticket intent routing, topic tagging, lead qualification, content categorization and feedback bucketing. For fixed category sets, see the [sentiment](https://sapling.ai/docs/api/sentiment.md) and [tone](https://sapling.ai/docs/api/tone.md) endpoints. [Try it out](https://sapling.ai/utilities/text-classifier?utm_campaign=apidocs) ## Sample Code[​](#sample-code "Direct link to Sample Code") * cURL * JavaScript * Python * Python (Sapling client) ``` curl -X POST https://api.sapling.ai/api/v1/classify \ -H "Content-Type: application/json" \ -d '{"key":"", "text":"Hi, I was charged twice for my March invoice. Can you refund the duplicate payment?", "labels": ["billing", "technical issue", "shipping", "other"], "context": "Support tickets for a SaaS billing product"}' ``` ``` import axios from 'axios'; async function run(text) { try { const response = await axios.post( 'https://api.sapling.ai/api/v1/classify', { key: '', text, labels: ['billing', 'technical issue', 'shipping', 'other'], context: 'Support tickets for a SaaS billing product', }, ); 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('Hi, I was charged twice for my March invoice. Can you refund the duplicate payment?'); ``` ``` import requests from pprint import pprint response = requests.post( "https://api.sapling.ai/api/v1/classify", json={ "key": "", "text": "Hi, I was charged twice for my March invoice. Can you refund the duplicate payment?", "labels": ["billing", "technical issue", "shipping", "other"], "context": "Support tickets for a SaaS billing product", } ) if 200 <= response.status_code < 300: pprint(response.json()) else: print('Error: ', response.status_code, response.text) ``` ``` from sapling import SaplingClient api_key = '' client = SaplingClient(api_key=api_key) text = 'Hi, I was charged twice for my March invoice. Can you refund the duplicate payment?' labels = [ {'name': 'billing', 'description': 'Charges, invoices, refunds and payment methods'}, {'name': 'technical issue', 'description': 'Bugs, errors, crashes and outages'}, {'name': 'shipping', 'description': 'Delivery status, addresses and tracking'}, {'name': 'other', 'description': 'Anything that does not fit the other labels'}, ] result = client.classify(text, labels, context='Support tickets for a SaaS billing product') print(result['label'], '|', result['rationale']) for entry in result['scores']: print(f"{entry['label']}: {entry['score']}") # Multi-label tagging: every label scoring >= threshold is returned in `labels` tags = client.classify( 'I was charged twice and the app crashes on launch', labels, multi_label=True, threshold=0.5, ) print(tags['labels']) # ['billing', 'technical issue'] ``` ## Sample Response[​](#sample-response "Direct link to Sample Response") ``` { "label": "billing", "labels": ["billing"], "scores": [ {"label": "billing", "score": 0.86}, {"label": "technical issue", "score": 0.07}, {"label": "shipping", "score": 0.04}, {"label": "other", "score": 0.03} ], "rationale": "The customer reports being charged twice on one invoice.", "multi_label": false } ``` With `"multi_label": true` and the text `"I was charged twice and the app crashes on launch"`, several labels can apply at once and the scores no longer need to sum to 1: ``` { "label": "billing", "labels": ["billing", "technical issue"], "scores": [ {"label": "billing", "score": 0.92}, {"label": "technical issue", "score": 0.81}, {"label": "shipping", "score": 0.03}, {"label": "other", "score": 0.02} ], "rationale": "Mentions a duplicate charge and an app crash.", "multi_label": true } ``` ## Batch Requests[​](#batch-requests "Direct link to Batch Requests") To classify many short texts — a queue of support tickets, a page of reviews — in one request, send `texts` (a list of 1–10 strings) instead of `text`. The same `labels`, `multi_label`, `threshold` and `context` 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/classify \ -H "Content-Type: application/json" \ -d '{"key":"", "texts": ["I was charged twice this month.", "Where is my package?"], "labels": ["billing", "shipping", "other"]}' ``` The batch response is `{"results": [...]}` with one entry per input, in input order; each entry has exactly the single-response shape above: ``` { "results": [ { "label": "billing", "labels": ["billing"], "scores": [ {"label": "billing", "score": 0.91}, {"label": "shipping", "score": 0.05}, {"label": "other", "score": 0.04} ], "rationale": "Reports a duplicate charge.", "multi_label": false }, { "label": "shipping", "labels": ["shipping"], "scores": [ {"label": "shipping", "score": 0.93}, {"label": "other", "score": 0.04}, {"label": "billing", "score": 0.03} ], "rationale": "Asks about a package's whereabouts.", "multi_label": false } ] } ``` 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 classify, 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. ## Request Parameters[​](#request-parameters "Direct link to Request Parameters") **POST** to `https://api.sapling.ai/api/v1/classify` **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 classify, up to 10,000 characters (measured on the raw input, before any HTML is stripped). Plain text or HTML — tags are stripped server-side. Text that is empty after stripping returns a `400`. The text is treated as untrusted data: instructions inside it are ignored and it is classified on its content alone. Provide exactly one of `text` and `texts`. **texts**: List\[String]
Batch form (see [Batch Requests](#batch-requests)): 1–10 texts to classify in one request, each treated exactly like `text` above, with a combined length of up to 10,000 characters. An item that is empty after stripping returns a `400` naming its index. The response becomes `{"results": [...]}`, one entry per item in input order. **labels**: List\[String | Object]
The set of labels to choose from: between 2 and 20 entries. Each entry is either a string (the label name) or an object `{"name": "...", "description": "..."}` where `description` is optional. Names are trimmed, internal whitespace is collapsed, and they must be at most 50 characters and case-insensitively unique; descriptions are at most 200 characters. Names are returned in this normalized form in the response, so match your application logic on the normalized names. A short description per label sharpens borderline decisions — see [Tips](#tips). **multi\_label**: Boolean, optional, defaults to false
`false`: exactly one label applies — scores are a probability distribution over the labels and `labels` is `[label]`. `true`: zero, one or several labels may apply — each label gets an independent 0–1 score and `labels` contains every label whose score is at least `threshold`. Non-boolean values return a `400`. **threshold**: Number, optional, defaults to 0.5
Multi-label cut-off between 0 and 1: a label is included in `labels` when `score >= threshold`. Ignored in single-label mode. Values outside 0–1 (or non-numbers) return a `400`. **context**: String, optional
Up to 500 characters describing what the texts are and how to decide between labels, for example `"Support tickets for a SaaS billing product; pick billing for anything about charges or refunds"`. Guidance only — it steers the decision but can never add a label outside `labels`. ## Response Parameters[​](#response-parameters "Direct link to Response Parameters") **label**: String
The best-matching label. Always one of the submitted label names (as normalized: trimmed, internal whitespace collapsed). **labels**: List\[String]
Single-label mode: `[label]`. Multi-label mode: every label whose score is at least `threshold`, in descending score order — this **may be empty**, which means none of the labels apply. **scores**: List\[Object]
Exactly one entry per submitted label, sorted by descending score (ties keep the model's pick first, then input order). Each has: * `label`: the label name. * `score`: 0–1, rounded to 4 decimals. In single-label mode the scores sum to \~1 (a probability distribution over the labels); in multi-label mode each is an independent applicability probability and they need not sum to 1. **rationale**: String
One sentence (at most 300 characters) naming the evidence in the text that drove the decision. May be an empty string. **multi\_label**: Boolean
Echo of the mode used for this request. ## Errors[​](#errors "Direct link to Errors") | Status | Meaning | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `400` | Validation error — missing or oversized `text`, text empty after stripping, both or neither of `text`/`texts` provided, an invalid or over-length `texts` batch, or invalid `labels` (e.g. `{"msg": "Invalid labels: Needs between 2 and 20 labels."}`), `multi_label`, `threshold` or `context`. The body is `{"msg": "..."}`. | | `401` / `403` | Missing or invalid API key. | | `429` | Rate limit exceeded or key over capacity. | | `502` | `{"msg": "Unexpected error classifying text."}` — the classification model failed; safe to retry. Not billed and not cached. | ## Tips[​](#tips "Direct link to Tips") * **Describe your labels.** Plain names work, but a one-line `description` per label (what belongs in it, and what does not) noticeably improves borderline cases, especially when labels overlap (`billing` vs `account`) or are domain-specific jargon. * **Add an "other" label.** In single-label mode the endpoint always picks one of your labels, so include a catch-all (`other`, `unrelated`, `none of the above`) unless every text is guaranteed to fit one of your categories. * **Use `context`** to say what the texts are and how to decide — e.g. `"Inbound sales emails; pick qualified only when a budget or timeline is mentioned"`. This is usually the cheapest way to fix systematic mistakes. * **Multi-label for tagging, single-label for routing.** Use `multi_label: true` when a text can legitimately belong to several buckets (topic tags, feature areas) and `false` when you need exactly one decision (queue assignment, intent). * **Tune `threshold` from the scores.** Start at the default `0.5`; lower it to recall more tags, raise it for precision. Results are cached on Sapling's side for about three days keyed on the text, labels, mode and context — the threshold is applied on top, so re-running with a different threshold returns the same underlying scores. * Scores are calibrated guidance, not guarantees — for high-stakes automation, act on high-confidence results and route low-margin ones (top two scores close together) to a human.   --- # 5 Best Grammar and Spell Checking APIs / SDKs [Try the Sapling API](https://sapling.ai/docs/api/edits-overview.md)       [Try the Sapling SDK](https://sapling.ai/docs/sdk/overview.md) Short answer Five grammar and spell checking APIs are compared here: **Bing** (spell check only, Azure, <0.3s), **Ginger** (REST, English dialects, rich edit metadata), **LanguageTool** (20+ languages, self-hostable, <2s), **ProWritingAid** (detailed error categories, SDKs in five languages), and **Sapling** (learning-based, self-serve keys, error categories, self-hosting, \~1s). Which is best depends on your use case — see [Which solution is best?](#which-solution-is-best) and the [note on evaluation](#note-on-evaluation) before picking one. ## Grammar check APIs[​](#grammar-check-apis "Direct link to Grammar check APIs") Grammar and spell checkers have a long history as part of document processing applications. Today, simple systems exist in our browsers, email clients, and even our mobile keyboards. However, these systems tend not to go beyond basic spellchecking and autocorrect, and even then often have frustratingly low accuracy. Today, popular systems such as Grammarly, Microsoft Editor, and Sapling offer functionality that goes beyond the built-in spellcheck of most applications. These systems offer browser and desktop integrations and make inline recommendations for ease of use. Of course, it's impossible to integrate across all applications or provide a customized experience for each application. If a one-size-fits-all integration doesn't suit your application, you may consider using an application programming interface (API). These allow your application to interface with an existing grammar and spell checking system through a communication standard. If you Google "grammar check API", many esoteric results will come up. This article tries to summarize the most useful APIs. For now, this covers English grammar check APIs, though we also include multilingual offerings in the future (Sapling supports many languages, which you can find [here](https://sapling.ai/langs?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs)). ![Example API flow](/img/api-flow.svg) Example of grammar checking API flow with input query and output result. ## Application programming interfaces[​](#application-programming-interfaces "Direct link to Application programming interfaces") While APIs have a rich history, in this article we'll assume that an API exposes a set of web **endpoints** of a grammar application. Your application can make HTTP **requests** to these web endpoints in order to fetch the set of edits for a particular piece of text. note You may have heard of **REST** APIs. Sapling (and many other APIs) follow the REST style, but you don't need to know the details of REST to use these systems. For example, suppose your application has a text input in which one of your users types the text: `Helo world`. Your system uses a third-party API to retrieve edits. It sends the following JSON to a third-party POST endpoint: ``` { "query": "Helo world" } ``` It then gets back the following JSON in response: ``` { "edits": [ { "start": 0, "end": 4, "replacement": "Hello" }, { "start": 5, "end": 10, "replacement": "world." } ] } ``` These two edits fix the first misspelling and also add a missing period. The third-party service has detected all the edits you need. It's then up to your system to render the edits on your frontend to your user in whatever manner you see fit. info In case you're interested in [autocomplete](https://sapling.ai/docs/api/autocomplete.md) and [similarity/semantic search](https://sapling.ai/docs/api/semsearch.md), Sapling also offers APIs for those as well — please contact us. ## Software development kits[​](#software-development-kits "Direct link to Software development kits") Note that in addition to APIs, some systems also offer SDKs that can make it even easier to integrate their system. For example, Sapling offers an [SDK](https://sapling.ai/docs/sdk/overview.md) that allows developers to integrate Sapling with 5 lines of JavaScript. We'll save more on SDKs for a separate article. ## Building your own[​](#building-your-own "Direct link to Building your own") Before we discuss providers, why use a third-party API instead of building your own? For example, there are great libraries out there such as [Stanza](https://stanfordnlp.github.io/stanza/) for preprocessing of text, and also deep learning libraries such as [TensorFlow](https://tensorflow.org/) and [PyTorch](https://pytorch.org/) that allow you to train your own system, and even higher-level libraries if you've decided on a specific model. ![Tensorflow](/img/comp/tensorflow.png) ![Pytorch](/img/comp/pytorch.png)
There are a few reasons why this is challenging. 1. *Language is really complex*. For every scenario you might come up with where you think a certain edit is appropriate, there are dozens if not hundreds of variations. If you're using a heuristic-based approach, you're likely not considering many of these variations. If you're using a machine learning-based approach, you'll need lots of data. 2. It'll take engineers some time to set up and maintain the system, and *engineers are expensive*. So too are servers if you'll be using a machine learning-based system that requires lots of compute. It's quite likely that using an API will not only be more time-efficient, but also cost-efficient. 3. You and your team have other things to focus on. If grammar correction and natural language processing are not a core competency, it's likely best to *focus* on your system's specialized offerings instead. Buying is almost always cheaper than building. As you likely rent cloud infrastructure from AWS or Azure, so too should you try and offload other services. ## Providers[​](#providers "Direct link to Providers") As of this writing, here are some common grammar checkers/writing assistant tools that offer API access: * Bing * Ginger * LanguageTool * ProWritingAid * Sapling We'll use the following broad criteria when evaluating the different APIs: * **Overview**: What type of API is offered? Does it do both grammar and spell checking, or exclusively spell checking? * **Strengths**: What type of suggestions are given and what is the performance in terms of accuracy and recall? * **Latency**: How long does it take for these systems to make suggestions for a typical query? This will of course depend on the document size, but parallelization should allow for scaling to large documents with latency then being the primary bottleneck. * **Other features**: What other suggestions or information beyond the edits does the system provide? What are the language and deployment options? Let's first give a brief introduction to the different systems. ### Bing[​](#bing "Direct link to Bing") ![Bing](/img/comp/bing.png)
The Bing Spell Check API helps correct spelling and adjust slang. It's especially helpful for brands and names as it also uses the Bing Search API. Types of corrections include word breaks, slang, names, homonyms, and brands. You can find out more about the Bing API on the Microsoft website: ### Ginger[​](#ginger "Direct link to Ginger") ![Ginger](/img/comp/ginger.png)
Ginger's grammar check API allows users to embed the Ginger system into their own application. While light on details, their information page mentions that it is used to check millions of documents a month. Given that the Ginger API provides a different interface to the Ginger platform, it's possible to get a sense of the performance by installing one of Ginger's other integrations, for example for the browser. Ginger is one of the grammar checking services that has been around longest — it was founded since 2010 — so you may have already tried its services and wish to try the API. ### LanguageTool[​](#languagetool "Direct link to LanguageTool") ![LanguageTool](/img/comp/languagetool.png)
LanguageTool is another of the most popular grammar checking programs that offers grammar, spelling, and style suggestions. LanguageTool has support for over 20 languages, though certain languages have a greater number of suggestions available. Its API offering can be hosted on-premises. It also offers the ability to specify the rules that should and should not be applied as well as set dictionaries. ### ProWritingAid[​](#prowritingaid "Direct link to ProWritingAid") ![ProWritingAid](/img/comp/prowritingaid.png)
ProWritingAid offers a grammar and spell check API for the system that also powers its own product. It offers SDKs in several languages, including C#, JavaScript, Python, PHP, and Java. Similar to Ginger, ProWritingAid is a popular solution that has been available since 2012, so you may be a user and wish to use the API as well. In addition to grammar and spell checking, ProWritingAid also offers thesaurus and text tagging endpoints. ### Sapling[​](#sapling "Direct link to Sapling") ![Sapling](/img/logo.svg)
Sapling is the language model copilot for businesses. Its technology is based off of research in the Stanford AI lab. The most recently developed of the solutions described here, Sapling is specifically geared towards business teams such as customer support teams or inside sales teams. Sapling uses a learning-based approach to catch up to [60% more errors](https://sapling.ai/comp?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs) than other systems. Users can provision a test API key themselves right after signing up for an account. You can find out more about [Sapling's grammar check API](https://sapling.ai/docs/api/edits-overview.md) in the API documentation. ## Comparison[​](#comparison "Direct link to Comparison") We've included a table below summarizing some of the benefits and drawbacks of the different systems. | Provider | Overview | Strengths | Latency | Other Features | | ------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ | ------------ | ------------------------------------------------------- | | Bing | Available with Azure subscription. Only supports spell checking. | Highly scalable system, leverages Bing Search API, multilingual support. | <0.3 seconds | Market code customization in addition to language code. | | Ginger | REST API, English dialects. | Detailed metadata for edits. | ? | Synonym and vocabulary recommendations. | | LanguageTool | REST API, multiple languages. | Support for many languages, self-hosting options. | <2 seconds | <2 seconds | | ProWritingAid | REST API, detailed error categories. | REST API, detailed error categories. | ? | Thesaurus, SDKs available, text tagging. | | Sapling | Machine learning-powered system designed for the enterprise. Self-serve. | Learning-based system that catches complex errors, self-hosting, multiple languages. | \~1 second | Error categories, self-hosting, and SDK available. | Comparison of API providers. ## Which solution is best?[​](#which-solution-is-best "Direct link to Which solution is best?") The best solution depends on your use case. Since there are many considerations here, we won't make a recommendation besides to try out each. But please consider the next section on evaluation. ## Note on evaluation[​](#note-on-evaluation "Direct link to Note on evaluation") Sometimes evaluation criteria are obvious — for example, to evaluate the latency of a system you would submit text of varying length and record the wall-clock time elapsed. info Constructing a sizable evaluation dataset with examples from real-world usage is the best way to judge the performance of different systems. From talking to users, we've frequently seen poor practices when it comes to evaluating different systems for accuracy and recall. Oftentimes, users will start by entering intentional errors such as the following: > Helo howare u. > fdsafsdafdsa. It seems natural that a spelling and grammar checking system should flag these errors. This approach, however, favors strict approaches, such as those that use simple dictionaries and lookup tables — let's call these **classical approaches** — instead of approaches that learn based off of real-world data — we'll call these **learning-based approaches**. Learning-based approaches are not trained on intentional errors, but actual errors seen in practice, and therefore can perform worse than classical approaches on blatant errors such as the ones in the example above. However, if you consider actual usage, learning-based approaches tend to capture many more errors and provide more useful suggestions. We recommend constructing your own evaluation dataset of at least a few dozen sentences containing errors by collecting example sentences that you or others on your team have seen. These examples should also be representative of examples you expect in the future. This is the best way to ensure that you get a good estimate of the actual performance of the systems you're testing and also to avoid cherry picking of examples. You can read more about evaluation [here](https://sapling.ai/docs/api/evaluation.md). ## Conclusion[​](#conclusion "Direct link to Conclusion") We recommend that you try out all of the different APIs and see what best fits your use case. This is because the requirements for a [medical application](https://sapling.ai/utilities/medical-spellcheck?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs) differ greatly from a legal application, which differ greatly from a customer service application. Obviously, we're biased. Get started with Sapling's documentation: Or you can reach out to us here: [https://sapling.ai/contact](https://sapling.ai/contact?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs) We're happy to talk through the customizations required for your particular use case. If you don't have time for a call, you can give Sapling a try using one of our our [prebuilt integrations](https://sapling.ai/integrations?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs).

Other tools we didn't mention? Please let us know: --- # advanced\_edits * cURL * JavaScript * Python ``` curl -X POST https://api.sapling.ai/api/v1/edits \ -H "Content-Type: application/json" \ -d '{ "key":"", "text":"The ball was kicked. Suddenly there was the sound of an aircraft.", "session_id": "test session", "advanced_edits": { "advanced_edits": true } }' ``` ``` import axios from 'axios'; async function run(text) { try { const response = await axios.post( 'https://api.sapling.ai/api/v1/edits', { key: '', session_id: 'test session', text, advanced_edits: { advanced_edits: true, }, }, ); const {status, data} = response; console.log({status}); console.log(JSON.stringify(data, null, 4)); } catch (err) { const { msg } = err.response.data; console.log({err: msg}); } } run('The ball was kicked. Suddenly there was the sound of an aircraft.'); ``` ``` import requests from pprint import pprint key = '' url = 'https://api.sapling.ai/api/v1/edits' data = { 'key': key, 'text': 'The ball was kicked. Suddenly there was the sound of an aircraft.', 'session_id': 'Test Document UUID', 'advanced_edits': { 'advanced_edits': True, }, } try: response = requests.post(url, json=data) if 200 <= response.status_code < 300: resp_json = response.json() edits = resp_json['edits'] pprint(edits) else: print('Error: ', str(response.status_code), response.text) except Exception as e: print('Error: ', e) ``` ### Sample Response[​](#sample-response "Direct link to Sample Response") ``` [ { "description": "Reduce the use of passive voice.", "end": 19, "error_type": "voice", "general_error_type": "Advanced Edit", "id": "81695ccc-2916-50ba-8b93-c3ed7bb55f54", "replacement": "", "sentence": "The ball was kicked.", "sentence_start": 0, "start": 9 }, { "description": "Reduce the use of adverbs.", "end": 8, "error_type": "adverbs", "general_error_type": "Advanced Edit", "id": "62cb63df-7c3d-52de-aab2-eded6924d689", "replacement": "", "sentence": "Suddenly there was the sound of an aircraft.", "sentence_start": 21, "start": 0 }, { "description": "Use simpler language where possible.", "end": 43, "error_type": "simplifications", "general_error_type": "Advanced Edit", "id": "a0367693-de21-524f-9a3e-abe7a62b46f6", "replacement": "plane", "sentence": "Suddenly there was the sound of an aircraft.", "sentence_start": 21, "start": 35 } ] ``` --- # ai\_detector #### Sample Code[​](#sample-code "Direct link to Sample Code") ``` import requests from pprint import pprint key = '' url = 'https://api.sapling.ai/api/v1/aidetect' data = { 'key': key, 'text': 'I am an artificial intelligence system designed to help people solve complex problems. My capabilities include natural language processing, machine learning, and predictive analytics.', } try: response = requests.post(url, json=data) if 200 <= response.status_code < 300: resp_json = response.json() pprint(resp_json) else: print('Error: ', str(response.status_code), response.text) except Exception as e: print('Error: ', e) ``` #### Sample Output[​](#sample-output "Direct link to Sample Output") ``` { "score": 0.9989707556330055, "ai_fraction": 1.0, "verdict": "ai", "segments": [ { "start": 0, "end": 183, "score": 0.985 } ], "low_confidence": true, "low_confidence_reasons": ["short_text"], "sentence_scores": [ { "score": 0.9764397893790885, "sentence": "I am an artificial intelligence system designed to help people solve complex problems.", "start": 0, "end": 86 }, { "score": 0.9923806397189778, "sentence": "My capabilities include natural language processing, machine learning, and predictive analytics.", "start": 87, "end": 183 } ], "text": "I am an artificial intelligence system designed to help people solve complex problems. My capabilities include natural language processing, machine learning, and predictive analytics." } ``` --- # apply\_edit\_javascript ``` function apply_edits(text, edits) { text = text.slice(); const reversed = edits.sort((a, b) => b['sentence_start'] + b['start'] - a['sentence_start'] - a['start']); for (const edit of reversed) { const start = edit['sentence_start'] + edit['start']; const end = edit['sentence_start'] + edit['end']; if (start > text.length || end > text.length) { console.log(`Edit start:${start}/end:${end} outside of bounds of text:${text}`); continue; } text = text.slice(0, start) + edit['replacement'] + text.slice(end); } return text; } ``` --- # apply\_edit\_php ``` function cmp($a, $b) { return $b->sentence_start + $b->start - $a->sentence_start - $a->start; } function apply_edits($text, $edits) { usort($edits, 'cmp'); foreach($edits as $edit) { $start = $edit->sentence_start + $edit->start; $end = $edit->sentence_start + $edit->end; if ($start > strlen($text) or $end > strlen($text)) { echo 'Index out of range'; continue; } $text = substr($text, 0, $start) + $edit->replacement + substr($text, $end, null); } return $text; } ``` --- # apply\_edit\_python ``` text = str(text) edits = sorted(edits, key=lambda e: (e['sentence_start'] + e['start']), reverse=True) for edit in edits: start = edit['sentence_start'] + edit['start'] end = edit['sentence_start'] + edit['end'] if start > len(text) or end > len(text): print(f'Edit start:{start}/end:{end} outside of bounds of text:{text}') continue text = text[: start] + edit['replacement'] + text[end:] return text ``` --- # apply\_edit * Python * JavaScript * PHP ``` text = str(text) edits = sorted(edits, key=lambda e: (e['sentence_start'] + e['start']), reverse=True) for edit in edits: start = edit['sentence_start'] + edit['start'] end = edit['sentence_start'] + edit['end'] if start > len(text) or end > len(text): print(f'Edit start:{start}/end:{end} outside of bounds of text:{text}') continue text = text[: start] + edit['replacement'] + text[end:] return text ``` ``` function apply_edits(text, edits) { text = text.slice(); const reversed = edits.sort((a, b) => b['sentence_start'] + b['start'] - a['sentence_start'] - a['start']); for (const edit of reversed) { const start = edit['sentence_start'] + edit['start']; const end = edit['sentence_start'] + edit['end']; if (start > text.length || end > text.length) { console.log(`Edit start:${start}/end:${end} outside of bounds of text:${text}`); continue; } text = text.slice(0, start) + edit['replacement'] + text.slice(end); } return text; } ``` ``` function cmp($a, $b) { return $b->sentence_start + $b->start - $a->sentence_start - $a->start; } function apply_edits($text, $edits) { usort($edits, 'cmp'); foreach($edits as $edit) { $start = $edit->sentence_start + $edit->start; $end = $edit->sentence_start + $edit->end; if ($start > strlen($text) or $end > strlen($text)) { echo 'Index out of range'; continue; } $text = substr($text, 0, $start) + $edit->replacement + substr($text, $end, null); } return $text; } ``` --- # chunk\_file\_python ``` from sapling import SaplingClient file_name = '' api_key = '' text = '' with open(file_name) as f: text = f.read().strip() client = SaplingClient(api_key=api_key) chunks = client.chunk_text(text, max_length=20000) for chunk in chunks: edits = client.edits(chunk, session_id=file_name) ``` --- # dei\_edits #### Sample Code[​](#sample-code "Direct link to Sample Code") ``` import requests key = '' url = 'https://api.sapling.ai/api/v1/edits' data = { 'key': key, 'text': 'This is the master bedroom.', 'session_id': 'Test Document UUID', 'advanced_edits': { 'dei': True, }, } try: resp = requests.post(url, json=data) if 200 <= resp.status_code < 300: resp_json = resp.json() edits = resp_json['edits'] print('Edits: ', edits) else: print('Error: ', str(resp.status_code), resp.text) except Exception as e: print('Error: ', e) ``` #### Sample Output[​](#sample-output "Direct link to Sample Output") ``` [ { "description": "Use more inclusive language where possible.", "end": 18, "error_type": "dei", "general_error_type": "Advanced Edit", "id": "fa409ba8-a51c-5d9e-b007-1bde1ac6183a", "replacement": "primary", "sentence": "This is the master bedroom.", "sentence_start": 0, "start": 12 } ] ``` --- # tone #### Sample Code[​](#sample-code "Direct link to Sample Code") ``` import requests key = '' url = 'https://api.sapling.ai/api/v1/tone' data = { 'key': key, 'text': 'You are amazing', } resp = requests.post(url, json=data) if 200 <= resp.status_code < 300: print(resp.json()) else: print('Error: ', str(resp.status_code), resp.text) ``` #### Sample Output[​](#sample-output "Direct link to Sample Output") ``` { "overall": [ [0.8885115385055542, "admiring", "😲"], [0.06413709372282028, "excited", "😀"], [0.02678699605166912, "approving", "👍"] ], "results": [ [ [0.8885115385055542, "admiring", "😲"], [0.06413709372282028, "excited", "😀"], [0.02678699605166912, "approving", "👍"] ] ], "sents": ["You are amazing"] } ``` --- # Custom Mappings Custom Mappings can be used to enforce specific corrections or capitalization for the following purposes: * Brand voice: Ensure that a consistent, on-brand style is used in all outward communication. * Compliance: If there is a specific way to communicate an issue, or if certain terms should be flagged, custom mappings can help. * Other use cases: Company or industry-specific jargon, product names, or unusual capitalization. ## Custom Mapping GET[​](#custom-mapping-get "Direct link to Custom Mapping GET") ### Request Parameters[​](#request-parameters "Direct link to Request Parameters") ``` import requests key = '' url = f'https://api.sapling.ai/api/v1/custom_mapping?key={key}' try: resp = requests.get(url) if 200 <= resp.status_code < 300: resp_json = resp.json() print('Custom mapping: ', resp_json) else: print('Error: ', str(resp.status_code), resp.text) except Exception as e: print('Error: ', e) ``` `https://api.sapling.ai/api/v1/custom_mapping?key=` HTTP method: `GET` **\**: 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. ### Response Parameters[​](#response-parameters "Direct link to Response Parameters") JSON array of custom mapping entries under key `custom_mappings`. Custom mapping entry structure: ``` { "id": , // Opaque id, used for deletion "entry": , // Word to match "mapping": , // Word to suggest as replacement "description": , // Shown with the suggestion; "" or null when none "case_sensitive": // true or false "created_at": // e.g. "Wed, 07 Oct 2020 21:15:47 GMT" "updated_at": // e.g. "Wed, 07 Oct 2020 21:15:47 GMT" } ``` ## Custom Mapping POST[​](#custom-mapping-post "Direct link to Custom Mapping POST") ### Request Parameters[​](#request-parameters-1 "Direct link to Request Parameters") ``` import requests key = '' url = 'https://api.sapling.ai/api/v1/custom_mapping' data = { 'key': key, 'entry': 'foobazbar', 'mapping': 'foobarbaz', 'case_sensitive': False, } try: resp = requests.post(url, json=data) if 200 <= resp.status_code < 300: resp_json = resp.json() print('Entry: ', str(resp_json)) else: print('Error: ', str(resp.status_code), resp.text) except Exception as e: print('Error: ', e) ``` `https://api.sapling.ai/api/v1/custom_mapping` HTTP method: `POST` **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. **entry**: String
Word to match. There are currently a couple types of regular expression-like options: * `^` and `$` match the start and end of a sentence, respectively. Example: `^the` will only match `the` at the start of a sentence and can be used to enforce capitalization. * `#` will match a number; example: `Gate #` will match in `Boarding is at Gate 5`. **mapping**: String
Word to suggest for a match. You can use `#` in the resulting expression to substitute a number back in. **case\_sensitive**: Boolean
Whether match is case-sensitive or not. If not case-sensitive, lower case and title case will both match. Defaults to true. **description**: Optional String
The description will be shown when the edit is suggested to the end user. If a description field is specified, the user will not have the option to ignore the edit. ### Response Parameters[​](#response-parameters-1 "Direct link to Response Parameters") JSON, Created Custom Mapping Entry Custom Mapping Entry Structure: ``` { "id": , // Opaque id, used for deletion "entry": , // Word to match "mapping": , // Word to suggest as replacement "description": , // Shown with the suggestion; "" or null when none "case_sensitive": // true or false "created_at": // e.g. "Wed, 07 Oct 2020 21:15:47 GMT" "updated_at": // e.g. "Wed, 07 Oct 2020 21:15:47 GMT" } ``` ### Errors[​](#errors "Direct link to Errors") The API answers 400, with the reason in `msg`, when: * the account already has a mapping for `entry` (`Entry already exists.`) * `entry` and `mapping` are the same * `entry` contains more than two digits * `case_sensitive` is false but `entry` and `mapping` differ only in case, e.g. `iphone` → `iPhone`. Such a rule must be case-sensitive. ## Custom Mapping DELETE[​](#custom-mapping-delete "Direct link to Custom Mapping DELETE") ### Request Parameters[​](#request-parameters-2 "Direct link to Request Parameters") ``` import requests key = '' entry_id = '' url = f'https://api.sapling.ai/api/v1/custom_mapping/{entry_id}' data = { 'key': key, } try: resp = requests.delete(url, json=data) if 200 <= resp.status_code < 300: print('Success') else: print('Error: ', str(resp.status_code), resp.text) except Exception as e: print('Error: ', e) ``` `https://api.sapling.ai/api/v1/custom_mapping/` HTTP method: `DELETE` **\**: String (UUID)
ID of custom mapping. **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. ### Response Parameters[​](#response-parameters-2 "Direct link to Response Parameters") Empty, HTTP status code 204. # Custom Filters Custom Filters allow you to ignore specific edits that match defined patterns during the editing process. This is useful for suppressing certain types of corrections that you don't want to see suggested. ## Custom Filter GET[​](#custom-filter-get "Direct link to Custom Filter GET") ### Request Parameters[​](#request-parameters-3 "Direct link to Request Parameters") ``` import requests key = '' url = f'https://api.sapling.ai/api/v1/custom_filter?key={key}' try: resp = requests.get(url) if 200 <= resp.status_code < 300: resp_json = resp.json() print('Custom filters: ', resp_json) else: print('Error: ', str(resp.status_code), resp.text) except Exception as e: print('Error: ', e) ``` `https://api.sapling.ai/api/v1/custom_filter?key=` HTTP method: `GET` **\**: 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. ### Response Parameters[​](#response-parameters-3 "Direct link to Response Parameters") JSON array of custom filter entries under key `custom_filters`. Custom filter entry structure: ``` { "id": , // Opaque id, used for deletion "created_at": , // e.g. "Wed, 07 Oct 2020 21:15:47 GMT" "updated_at": , // e.g. "Wed, 07 Oct 2020 21:15:47 GMT" "input": , // Input text to match (can be empty string for wildcards) "output": , // Output text to match (can be empty string for wildcards) "case_sensitive": // true or false } ``` ## Custom Filter POST[​](#custom-filter-post "Direct link to Custom Filter POST") ### Request Parameters[​](#request-parameters-4 "Direct link to Request Parameters") ``` import requests key = '' url = 'https://api.sapling.ai/api/v1/custom_filter' data = { 'key': key, 'inp': 'Patient', 'out': 'The patient', 'case_sensitive': False, } try: resp = requests.post(url, json=data) if 200 <= resp.status_code < 300: resp_json = resp.json() print('Filter created: ', resp_json) else: print('Error: ', str(resp.status_code), resp.text) except Exception as e: print('Error: ', e) ``` `https://api.sapling.ai/api/v1/custom_filter` HTTP method: `POST` **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. **inp**: String
Input text to match. Can be empty string for wildcards. **out**: String
Output text to match. Can be empty string for wildcards. **case\_sensitive**: Boolean
Whether the filter should be case-sensitive. Defaults to true. ### Response Parameters[​](#response-parameters-4 "Direct link to Response Parameters") JSON, Created Custom Filter Entry Custom Filter Entry Structure: ``` { "id": , // Opaque id, used for deletion "created_at": , // e.g. "Wed, 07 Oct 2020 21:15:47 GMT" "updated_at": , // e.g. "Wed, 07 Oct 2020 21:15:47 GMT" "input": , // Input text to match "output": , // Output text to match "case_sensitive": // true or false } ``` ### Errors[​](#errors-1 "Direct link to Errors") The API answers 400, with the reason in `msg`, when: * `inp` and `out` are both blank after trimming whitespace * `inp` and `out` are the same * the account already has a filter with this `inp` and `out` (`Filter already exists.`) * `inp` contains more than two digits * `case_sensitive` is false but `inp` and `out` differ only in case, e.g. `apple` → `Apple`. Such a filter must be case-sensitive. ## Custom Filter DELETE[​](#custom-filter-delete "Direct link to Custom Filter DELETE") ### Request Parameters[​](#request-parameters-5 "Direct link to Request Parameters") ``` import requests key = '' filter_id = '' url = f'https://api.sapling.ai/api/v1/custom_filter/{filter_id}' data = { 'key': key, } try: resp = requests.delete(url, json=data) if 200 <= resp.status_code < 300: print('Success') else: print('Error: ', str(resp.status_code), resp.text) except Exception as e: print('Error: ', e) ``` `https://api.sapling.ai/api/v1/custom_filter/` HTTP method: `DELETE` **\**: String (UUID)
ID of custom filter to delete. **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. ### Response Parameters[​](#response-parameters-5 "Direct link to Response Parameters") Empty, HTTP status code 204. --- # AI Detector API Sapling's **AI detector API** computes the probability that a piece of text is AI-generated (sometimes called "AI slop"), as well as the probability that each constituent sentence and token is AI-generated. Use this AI detection API to flag machine-generated content in user submissions, marketplace listings, reviews, job applications, student work, and content pipelines — or as an AI slop detector API for identifying low-quality, machine-generated content. The system is trained to be able to handle LLMs from different vendors, such as OpenAI's GPT family of models, Google's Gemini models, Anthropic's Claude models, and the open-release Llama and Mistral models. It is also somewhat robust to small changes and noisy text. | AI detection API | At a glance | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Endpoint** | `POST https://api.sapling.ai/api/v1/aidetect` | | **Returns** | Document score from 0 to 1, the fraction of the text that reads as AI-generated, per-sentence scores with offsets, per-token probabilities, and an optional HTML heatmap | | **Input limits** | Up to 200,000 characters per request; at least 300 characters recommended | | **Languages** | English, Spanish, Portuguese, French, German, Italian, Dutch and Japanese, detected automatically (see [Languages](#languages)) | | **SDKs** | [Python](https://sapling.ai/docs/sdk/Python/documentation.md), [JavaScript](https://sapling.ai/docs/sdk/JavaScript/ai_detect_quickstart.md) | | **Pricing** | From $0.005 per 1,000 characters, with volume discounts — see [API Pricing](https://sapling.ai/docs/api/pricing) | [Run Detector](https://sapling.ai/utilities/ai-content-detector?utm_campaign=apidocs) Accuracy and Trade-offs All AI detection systems have false positives and false negatives. In some cases, small modifications to AI-generated text can cause that text to no longer be flagged as AI-generated. In other cases, human-written (but perhaps rote) text can be misclassified as AI-generated. Please do not interpret this as conclusive belief that your text is AI slop. Depending on the application, false positives or false negatives may be less desirable. Contact us for ways to adjust for your use case. ## Quickstart[​](#quickstart "Direct link to Quickstart") 1. [Register for a Sapling account](https://sapling.ai/user/register?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs) and generate a key from your [API settings dashboard](https://sapling.ai/api_settings?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs). See [API Access](https://sapling.ai/docs/api/api-access.md) for the full walkthrough. 2. `POST` your text to `https://api.sapling.ai/api/v1/aidetect` with your key, using one of the samples below. 3. Read `score` from the response — closer to `1` means more confidence the text is AI-generated. Read `ai_fraction` to see how much of a document reads as AI-generated (useful when only part of it is), and use `segments`, `sentence_scores` or `token_probs` to show *where* in the text that comes from. `verdict` sums the result up as `human`, `mixed` or `ai`, and `low_confidence` tells you when to hedge it. ## Sample Code[​](#sample-code "Direct link to Sample Code") * cURL * JavaScript * Python * Python SDK ``` curl -X POST https://api.sapling.ai/api/v1/aidetect \ -H "Content-Type: application/json" \ -d '{"key":"", "text":"This is sample text."}' ``` ``` import axios from 'axios'; async function run(text) { try { const response = await axios.post( 'https://api.sapling.ai/api/v1/aidetect', { key: '', text, }, ); const {status, data} = response; console.log({status}); console.log(JSON.stringify(data, null, 4)); } catch (err) { const { msg } = err.response.data; console.log({err: msg}); } } run('This is sample text'); // replace with the text you want to analyze ``` ``` import requests from pprint import pprint response = requests.post( "https://api.sapling.ai/api/v1/aidetect", json={ "key": "", "text": "This is sample text." } ) if 200 <= response.status_code < 300: pprint(response.json()) else: print('Error: ', response.status_code, response.text) ``` ``` # pip: python -m pip install sapling-py # uv: uv add sapling-py from sapling import SaplingClient from pprint import pprint api_key ='' client = SaplingClient(api_key=api_key) detection_scores = client.aidetect('This is sample text.', sent_scores=True) pprint(detection_scores) ``` AI Detector UI integration Sapling's Javascript SDK provides a complete end-to-end UI integration for AI content detection capabilities. Head over to our **[AI Detect JavaScript Quickstart ](https://sapling.ai/docs/sdk/JavaScript/ai_detect_quickstart.md)**for more details. ## AI Detector POST[​](#ai-detector-post "Direct link to AI Detector POST") ### Request Parameters[​](#request-parameters "Direct link to Request Parameters") `https://api.sapling.ai/api/v1/aidetect` HTTP method: `POST` The AI Detector API POST endpoint takes JSON parameters documented below: **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
Text to run detection on. The limit is currently **200,000 characters**. If latency is high or requests time out, we recommend adapting [this script](https://github.com/saplingai/sapling-samples/blob/master/python/scripts/get_ai_detection.py). Please contact us if you need to run the system on longer inputs. We can also provide suggestions on how to chunk your text into smaller pieces and then combine detection results. Provide exactly one of `text` and `texts`. **texts**: List\[String]
Batch form (see [Batch Requests](#batch-requests)): 1–10 texts to run detection on in one request, each scored independently under the same options. **sent\_scores**: Boolean
Whether to return sentence scores. Defaults to `true`. Sentence scores are computed from the same model pass as the document score, so they add almost no latency. **score\_string**: Boolean
Whether to return string highlighting token-level scores. Defaults to `false`. This allows you to visualize which portions of the text are likely AI-generated similar to on Sapling's [AI detector page](https://sapling.ai/ai-content-detector?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs). **lang**: String
Optional language of the text: `en`, `es`, `pt`, `fr`, `de`, `it`, `nl` or `ja` (region subtags such as `pt-BR` are accepted). Omit it, or pass `auto`, to detect the language automatically (see [Languages](#languages)). Setting it picks that language's model instead of relying on detection, which helps with short texts. Unsupported languages return a `400` error. If `version` is also set, `version` wins. **version**: String
There are currently 4 versions of the detector available: 1. `20240606` (only available upon request) 2. `20251027` (previous default; available by specifying this version) 3. `20260820` (current default) 4. `ml20261009` (multilingual model for English, Spanish, Portuguese, French, German, Italian, Dutch and Japanese) Requests without `version` are routed by language: text detected as one of the [supported non-English languages](#languages) is scored by `ml20261009`, everything else by the default model. Pass `lang` to specify the language explicitly. Specifying a `version` turns this routing off, and every text uses that model. **zero\_retention**: Boolean
Optional. Set it to `true` to keep no copy of the text. See [Data retention](#data-retention). Defaults to `false`; it is always on for teams with data retention disabled. A non-boolean value returns a `400` error. In a batch request it applies to every item. **create\_report**: Boolean
Optional. Set it to `true` to also save the result as a shareable report and get its link in `report`. See [Shareable reports](#shareable-reports). Single `text` requests only; it can't be combined with `zero_retention` or `texts` (`400`). It needs your private API key: OAuth access tokens and public API keys get a `400`. ### September 17, 2026 default-model upgrade[​](#september-17-2026-default-model-upgrade "Direct link to September 17, 2026 default-model upgrade") The default AI Detector model was upgraded to version `20260820` on September 17, 2026. The upgrade brings higher precision on modern text and better recall on humanized AI text. In our internal held-out evaluations, human-text false positives fell by 60% on the modern-text set, while detection of humanized AI text improved by 3.9% absolute. We've also refreshed training data from recent ChatGPT, Claude, Gemini, DeepSeek, and Qwen models, and added Kimi coverage. Requests that omit the version parameter and aren't [routed to the multilingual model](#languages) use the new model. To keep using the previous model, include `"version": "20251027"` in each request. ### October 2026 update: `ai_fraction` and model-based sentence scores[​](#october-2026-update-ai_fraction-and-model-based-sentence-scores "Direct link to october-2026-update-ai_fraction-and-model-based-sentence-scores") Two changes to the response, with no change to the request or to `score`: * **New `ai_fraction` field** on every response: the share of the text (from 0 to 1) in sentences scoring `0.5` or above. The document `score` measures how AI-like the text is *as a whole*, so a document that is half human-written and half AI-generated gets a low `score`. `ai_fraction` reports roughly `0.5` for it, so partially AI-written documents are no longer hidden. * **`sentence_scores` now come from the detector model itself**, on the same scale and with the same `0.5` threshold as `score`. Each entry also includes `start` and `end` character offsets into `text` for highlighting. Previously, sentence scores were computed by a separate method and could disagree with the overall score. Sentence boundaries may also differ slightly from before. Requests with default options are also faster, since sentence scoring no longer needs a second model pass. ### Response Parameters[​](#response-parameters "Direct link to Response Parameters") The AI Detector POST endpoint returns JSON of the following format: ``` { "score": 0.8016229165451867, "ai_fraction": 1.0, "verdict": "ai", "segments": [ { "start": 0, "end": 19, "score": 0.8016 } ], "low_confidence": true, "low_confidence_reasons": ["short_text"], "sentence_scores": [ { "score": 0.8016229165451867, "sentence": "Here is a sentence.", "start": 0, "end": 19 } ], "text": "Here is a sentence.", "token_probs": [ 0.8062431365251541, 0.8068526238203049, 0.8062431365251541, 0.8080672174692154, 0.8062431365251541 ], "tokens": [ "Here", " is", " a", " sentence", "." ], "version": "..." } ``` A `score` from 0 to 1 will be returned, with 0 indicating the maximum confidence that the text is human-written, and 1 indicating the maximum confidence that the text is AI-generated. **ai\_fraction**: The fraction of the text, from 0 to 1, in sentences whose score is `0.5` or above (weighted by characters, ignoring whitespace). Use it alongside `score` for documents that may be only partly AI-generated: a document with one AI-generated paragraph out of two has a low `score` but an `ai_fraction` near `0.5`. It is `null` in the rare case that no sentence could be scored. **verdict**: A plain-language summary of `score` and `ai_fraction`, one of: * `ai`: `score` is `0.5` or above and at least 80% of the text (`ai_fraction` ≥ `0.8`) scores as AI-generated. * `mixed`: otherwise, `score` is `0.5` or above or at least 20% of the text (`ai_fraction` ≥ `0.2`) scores as AI-generated. This catches partly AI-written documents whose overall `score` stays low. * `human`: everything else. When `ai_fraction` is `null`, the verdict is `ai` or `human` by `score` alone. `verdict` is omitted if the text could not be scored. **segments**: The AI-generated passages: each run of consecutive sentences scoring `0.5` or above, as an object such as `{"start": 0, "end": 183, "score": 0.985}`, in text order. `start`/`end` are character offsets into `text` counted like the `sentence_scores` offsets below, and `score` is the run's sentence scores averaged by length. It is an empty list when no sentence scores `0.5` or above. Segments are returned even with `sent_scores` set to `false`, so you can highlight AI-generated passages without the per-sentence list. They are omitted when sentence offsets are unavailable. **low\_confidence**: `true` when the input is of a kind where human-written text is more often misread as AI-generated, so the result should be shown with a caveat rather than as a bare score. **low\_confidence\_reasons** lists why (empty when `low_confidence` is `false`): * `short_text`: the text is under 500 characters. * `list_format`: most of the text is bullet or numbered lines. If `score_string` is set to `true`, a `score_string` field will be provided. The field contains an HTML string with a heatmap of the portions of the text that are predicted to be AI-generated. If the default score string is not what you desire, you can generate your own using `tokens` and `token_probs`. If `sent_scores` is `true` (the default), a field `sentence_scores` containing scores for each sentence will also be returned. Each entry contains the `sentence`, its `score` (which uses the same model, scale, and `0.5` threshold as the overall `score`), and the `start`/`end` character offsets of the sentence in `text`, so `text[start:end]` equals `sentence` (`end` is exclusive). Offsets count Unicode code points (Python `str` indexing), not UTF-16 code units or bytes. In JavaScript, slice with `Array.from(text)` if the input may contain characters outside the Basic Multilingual Plane (e.g. emoji), whose UTF-16 string indices would otherwise misalign. The overall `score` is computed over the whole text at once, so it is not the plain average of the sentence scores. **tokens**: List of tokens from backend tokenizer that can be used to `token_probs` to visualize the output prediction per token. **token\_probs**: List of probabilities that each token is AI-generated. This can be used with `tokens` to visualize the output prediction per token. **version**: Identifies the detector that scored the text: the `version` you requested or, if you didn't pin one, the model the text was [routed to](#languages) (the current default model, or `ml20261009` for supported non-English text). Treat it as an opaque string, and log it if you need to know which model produced a stored result. **language** (optional): When the text's language is identified with high confidence, an object such as `{"code": "es", "name": "Spanish", "confidence": 0.98}`. Use it with `version` to see which model scored the text, or to flag results for [unsupported languages](#languages). It is omitted when the language can't be identified confidently, so don't rely on it being present. ### Batch Requests[​](#batch-requests "Direct link to Batch Requests") To score many texts — a queue of submissions, a page of reviews — in one request, send `texts` (a list of 1–10 strings) instead of `text`. The same `sent_scores`, `score_string`, and `version` options, as well as `lang` (if set), apply to every item. Exactly one of `text` and `texts` must be provided, and the **combined** length of all items may be up to 200,000 characters (the same cap as a single `text`). ``` curl -X POST https://api.sapling.ai/api/v1/aidetect \ -H "Content-Type: application/json" \ -d '{"key":"", "texts": ["First submission to check.", "Second submission to check."], "sent_scores": false}' ``` The batch response is `{"results": [...]}` with one entry per input, in input order; each entry has exactly the single-response shape above (`sentence_scores`, `tokens`, and `token_probs` are relative to that item's own text): ``` { "results": [ {"score": 0.93, "ai_fraction": 1.0, "verdict": "ai", "segments": [{"start": 0, "end": 26, "score": 0.93}], "low_confidence": true, "low_confidence_reasons": ["short_text"], "text": "First submission to check.", "tokens": ["..."], "token_probs": [0.9], "version": "..."}, {"score": 0.04, "ai_fraction": 0.0, "verdict": "human", "segments": [], "low_confidence": true, "low_confidence_reasons": ["short_text"], "text": "Second submission to check.", "tokens": ["..."], "token_probs": [0.1], "version": "..."} ] } ``` Each item is scored independently and billed individually (a batch of N costs the same as N single requests, and identical items are billed once). If any item fails, the whole request returns an error and nothing is billed; items that did resolve are cached, so a retry only re-scores the rest. The batch form requires an API key. ### Interpreting the score[​](#interpreting-the-score "Direct link to Interpreting the score") `score` is a probability estimate, not a decision. How you turn it into one depends on which kind of mistake is more costly in your application: * **A high threshold** (for example, only act above `0.9`) minimizes false accusations, at the cost of letting more AI-generated text through. This is usually the right default when a flag has consequences for a person — moderation, admissions, hiring. * **A lower threshold** surfaces more candidates for human review. This suits content pipelines where a flag just means "route to an editor". * **`ai_fraction` and sentence scores** are useful for review workflows: a document that is mostly human-written with a few AI-generated paragraphs has a low overall `score`, but its `ai_fraction` and the flagged entries in `sentence_scores` show how much of it was AI-generated, and where. Consider flagging on `ai_fraction` (for example, above `0.3`) when partially AI-written submissions matter to you. `segments` gives the same passages ready to highlight. * **`verdict`** is a ready-made label for display, using a `0.5` threshold on `score` and `ai_fraction` cut-offs of `0.2` and `0.8`. Use your own threshold instead when you need a stricter or looser trade-off than that. * **`low_confidence`** marks inputs where false positives are more likely: short text and bullet-list text. When it is `true`, show the result with a caveat or ask for more text rather than acting on the score alone. Contact us if you need help calibrating thresholds against your own labelled data. ### Languages[​](#languages "Direct link to Languages") The detector supports English, Spanish, Portuguese, French, German, Italian, Dutch and Japanese. Sapling identifies the language of each text (from its first 20,000 characters) and, when it is confident (above 0.9) and no `version` is pinned, scores Spanish, Portuguese, French, German, Italian, Dutch and Japanese text with a multilingual model trained on human-written and AI-generated text. Italian and Dutch are newer: the model has no Italian or Dutch training data yet, so in Sapling's evaluation it flags about as little human-written text as in the other languages but catches somewhat less AI-generated text. The detected language is returned in the `language` field and the model in `version`. Without a pinned `version`, a document that mixes languages is routed by its dominant language. If you already know the language, pass it as [`lang`](#request-parameters) to use that language's model directly. If you don't pin a `version`, text in other languages, or text whose language can't be identified confidently, is scored by the English model. Those scores are less reliable, so treat them with more caution. To try the detector in a browser, use the [English](https://sapling.ai/ai-content-detector?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs), [Spanish](https://sapling.ai/es/detector-de-ia?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs), [Portuguese](https://sapling.ai/pt/detector-de-ia?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs), [French](https://sapling.ai/fr/detecteur-ia?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs), [German](https://sapling.ai/de/ki-detektor?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs), [Italian](https://sapling.ai/it/rilevatore-ia?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs) or [Japanese](https://sapling.ai/ja/ai-checker?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs) detector page. ### Limits, quotas, and pricing[​](#limits-quotas-and-pricing "Direct link to Limits, quotas, and pricing") * **Input length**: up to 200,000 characters per request. At least 300 characters is recommended — it is possible to classify shorter text, but there is much less signal to work with. Results on text under 500 characters have `low_confidence` set to `true`. * **Free quota**: new API keys get 50,000 characters/day and 250,000 characters/month. See [Usage and Pricing](https://sapling.ai/docs/usage-pricing.md). * **Rate limits**: each API key has its own character allowance for this endpoint, refilled continuously: 50,000 characters every 24 hours for free-trial keys and, by default, 10 million characters every 24 hours for subscribed keys. The monthly quota applies on top. A single request longer than the key's whole allowance gets a `429` without `retry_after` and can't succeed as sent. Use the `X-RateLimit-*` response headers to pace batch jobs. See [Rate Limits](https://sapling.ai/docs/rate_limits.md). * **Caching**: AI detection results are cached on the entire text for 3 days, so re-sending an identical document does not incur additional usage. See [API Pricing](https://sapling.ai/docs/api/pricing). Requests with [`zero_retention`](#data-retention) are not cached. * **Billing**: usage-based, per character, starting at $0.005 per 1,000 characters with volume discounts. See [API Pricing](https://sapling.ai/docs/api/pricing). ### Data retention[​](#data-retention "Direct link to Data retention") Send `"zero_retention": true` to score a text without Sapling keeping a copy of it. The request is scored and billed as usual, but: * **Not cached**: the result is not written to the 3-day result cache. If someone else's request already cached the identical text, that cached result can still be returned. * **No stored text in usage records**: usage is logged without the text. A hash of the text is kept, so sending it again within the deduplication window is still not billed twice. * **No samples or history**: the request is never sampled for quality review or saved to detector history. It works on every AI detection endpoint: `/api/v1/aidetect` (single and batch), [`/api/v1/url-aidetect`](#checking-web-pages) and [`/api/v1/file-aidetect`](#checking-files), which takes it in `jsonParams`. Teams with data retention disabled get this behavior on every request, including checks from the Sapling extensions and the Word add-in, without setting the flag. For retention policies across all of Sapling, see [Data Processing](https://sapling.ai/docs/data.md). ``` curl -X POST https://api.sapling.ai/api/v1/aidetect \ -H "Content-Type: application/json" \ -d '{"key": "", "text": "", "zero_retention": true}' ``` ### Shareable reports[​](#shareable-reports "Direct link to Shareable reports") Send `"create_report": true` to save a result as a report you can share, like the links the [AI detector dashboard](https://sapling.ai/ai-detector?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs) creates. Alongside the usual fields, the response then carries: ``` { "report": { "id": "3f0c...", "url": "https://sapling.ai/ai-detector/report/", "shared_at": "2026-10-27T12:00:00Z", "expires_at": "2027-01-25T12:00:00Z" } } ``` Anyone with `url` can view the scored text, its overall score, verdict and highlighted passages, without signing in. The page never shows your name, email or API key. * **Where it lives**: the report is saved to your AI detector history (source "API"), where you can revoke the link or delete the entry. It counts toward your history limits; when history is full, the oldest entries and their links are removed first. * **How long it lasts**: until `expires_at`, 90 days after it was created, unless you revoke the link, delete the entry, or turn off text retention for your history sooner. Cancelling a plan doesn't revoke links, but history above the free plan's limit (100 entries) is removed, oldest first, the next time a check is saved. * **When no report is made**: the request is still scored and billed, `report` is `null`, and `report_skip_reason` says why: `nonpersist` (your team keeps no data), `disabled` (AI detector history is turned off), `text_not_retained` (history keeps no text), `unconfirmed_email`, `quota`, `unscorable` or `error`. It works on `/api/v1/aidetect` (single `text`), [`/api/v1/url-aidetect`](#checking-web-pages) and [`/api/v1/file-aidetect`](#checking-files), which takes it in `jsonParams`. Because a report keeps the text, `create_report` can't be combined with `zero_retention`. Because it publishes a link from your history, it needs your private API key, sent from a server. OAuth access tokens (such as an MCP client's) and public API keys get a `400` and nothing is scored. ``` curl -X POST https://api.sapling.ai/api/v1/aidetect \ -H "Content-Type: application/json" \ -d '{"key": "", "text": "", "create_report": true}' ``` ### Tips[​](#tips "Direct link to Tips") * Check the status code of the result and any error logs. * Unless you're sending very large requests, the requests should rarely fail or time out, but you can follow [these instructions to implement a retry mechanism](https://www.zenrows.com/blog/python-requests-retry#use-existing-retry-wrapper). ### Checking Files[​](#checking-files "Direct link to Checking Files") To score a document without extracting its text yourself, upload it to the file endpoint. It reads the document's text and scores it as if you had sent that text to `/api/v1/aidetect` with `sent_scores` and `score_string` enabled. **POST** to `https://api.sapling.ai/api/v1/file-aidetect` as `multipart/form-data`. * **`file`** (required): the document. The format is chosen by file extension: `.pdf`, `.docx`, `.pptx`, `.odt`, `.txt`, `.md`, `.html` or `.htm`. Uploads are limited to 30MB; a larger file returns `413`. * **`jsonParams`**: a JSON object holding your key, `{"key": ""}`, sent as a file part or as a plain form field. You can send the key as a bearer token in the `Authorization` header instead. It can also hold `"zero_retention": true` (see [Data retention](#data-retention)) or `"create_report": true` (see [Shareable reports](#shareable-reports)). ``` curl -X POST https://api.sapling.ai/api/v1/file-aidetect \ -F "file=@essay.pdf" \ -F 'jsonParams={"key":""}' ``` ``` import json import requests with open("essay.pdf", "rb") as f: response = requests.post( "https://api.sapling.ai/api/v1/file-aidetect", files={"file": f}, data={"jsonParams": json.dumps({"key": ""})}, ) print(response.status_code, response.json()) ``` The response has the same fields as a `/api/v1/aidetect` response, including `score`, `verdict`, `segments`, `low_confidence`, `sentence_scores`, `score_string` and `language`. Some things to know: * **Text read**: only the first 200,000 characters of extracted text are scored. * **What is extracted**: * PPTX: slides are read in order; speaker notes are skipped. * ODT: footnotes, comments and tracked deletions are dropped. * HTML: only the visible text is read. * **Language**: the document's language is detected and routed exactly as for text requests (see [Languages](#languages)). This endpoint does not take `lang` or `version`. * **Billing**: billed like `/api/v1/aidetect` on the extracted text. * **Rate limit**: 60 requests per minute per account. The limit is shared with the other upload endpoints. * **Errors**: file problems come back as `400` `{"error": ""}`: * the `file` part is missing * the extension is unsupported * the document is empty, encrypted, corrupt or has no extractable text * `zero_retention` in `jsonParams` is not a boolean A request with no key at all also returns `400` `{"error": "Unauthenticated"}`. A transient scoring failure is also a `400`, with an "Unexpected error" message in `error` or `msg`. You can retry it after a short delay. The older `/api/v1/pdf-aidetect` and `/api/v1/docx-aidetect` routes still work, but new integrations should use `/api/v1/file-aidetect`. To pull typed fields out of a document instead of scoring it, see [Data Extraction](https://sapling.ai/docs/api/extract.md#file-uploads). ### Checking Web Pages[​](#checking-web-pages "Direct link to Checking Web Pages") To score a public web page, send its URL. Sapling fetches the page, keeps its main readable text and scores that text like a `/api/v1/aidetect` request. Navigation, headers, footers, forms and hidden elements are dropped, and the largest `
` (else `
`) is preferred, so the cookie banners and footer links you would get by scraping the raw HTML yourself are neither scored nor billed. **POST** to `https://api.sapling.ai/api/v1/url-aidetect` with a JSON body: * **`key`**: your API key (or send it as a bearer token in the `Authorization` header). * **`url`** (required): the page to check, up to 2,048 characters. A bare host and path such as `example.com/blog/post` gets `https://` added. * **`sent_scores`**, **`score_string`**, **`version`**, **`lang`**, **`zero_retention`**, **`create_report`**: the same options as [`/api/v1/aidetect`](#request-parameters). ``` curl -X POST https://api.sapling.ai/api/v1/url-aidetect \ -H "Content-Type: application/json" \ -d '{"key": "", "url": "https://example.com/blog/post", "sent_scores": false}' ``` ``` import requests response = requests.post( "https://api.sapling.ai/api/v1/url-aidetect", json={"key": "", "url": "https://example.com/blog/post", "sent_scores": False}, timeout=60, ) print(response.status_code, response.json()) ``` The response has the same fields as a `/api/v1/aidetect` response, plus a `source` object describing what was scored: ``` { "score": 0.93, "ai_fraction": 0.88, "verdict": "ai", "segments": [{"start": 0, "end": 4810, "score": 0.97}], "low_confidence": false, "low_confidence_reasons": [], "text": "...", "version": "...", "source": { "url": "https://example.com/blog/post", "title": "Example post", "content_type": "text/html", "characters": 5120, "truncated": false } } ``` * **`source.url`**: the final URL after redirects. * **`source.title`**: the page's `og:title`, `` or first `<h1>`. For a linked document, this is its file name. * **`source.content_type`**: the media type from the server's `Content-Type` header, such as `text/html` or `application/pdf`. A document served with a generic type is still read by its URL extension, so this can be `application/octet-stream`. * **`source.characters`**: the characters scored, which is what you are billed for. * **`source.truncated`**: `true` when only a prefix was scored. Some things to know: * **Linked documents**: a URL that serves a `.pdf`, `.docx`, `.pptx`, `.odt`, `.txt` or `.md` file is read like an upload to [`/api/v1/file-aidetect`](#checking-files). The format is chosen by `Content-Type`, or by the URL's extension when the server sends a generic type. * **Size limits**: the first 2MB of an HTML page are read, a document over 15MB is rejected, and only the first 200,000 characters of text are scored. * **What can be fetched**: only `http` and `https` URLs on ports 80, 443, 8080 or 8443 that resolve to public addresses. Up to 5 redirects are followed, and each one is checked the same way. Pages behind a login or paywall can't be read: send their text to `/api/v1/aidetect` instead. * **Billing**: billed like `/api/v1/aidetect` on the characters scored. Results are cached on the scored text, so checking an unchanged page again does not use more quota. * **Rate limits**: 30 URL checks per minute per account, on top of the [`/api/v1/aidetect` limits](#limits-quotas-and-pricing). URL checks share `/api/v1/aidetect`'s character allowance, and each check counts at least 5,000 characters against it. * **Errors** (all `{"msg": "<reason>"}`): * `400`: the URL is malformed, isn't allowed (wrong scheme or port, or a host that doesn't resolve to a public address), or redirects too many times; or `lang`, `version` or `zero_retention` is invalid. * `422`: the page was fetched but can't be scored. It answered with a non-2xx status (many sites answer `403` to automated requests), has an unsupported content type, is an oversized or unreadable document, or has no readable text. * `502`: the page couldn't be fetched (a timeout or connection failure). You can retry it later. As with text requests, a transient scoring failure is a `400` with an "Unexpected error" message; retry it after a short delay. From an AI assistant, the `sapling_ai_detect_url` tool on the [Sapling MCP server](https://sapling.ai/docs/mcp.md) makes the same request and returns the same fields. A page that can't be fetched or scored comes back as a tool error carrying the reason, not as a score. ## Other ways to use Sapling's AI detection[​](#other-ways-to-use-saplings-ai-detection "Direct link to Other ways to use Sapling's AI detection") Beyond calling the HTTP API directly, the same detector is available through: * **[JavaScript SDK](https://sapling.ai/docs/sdk/JavaScript/ai_detect_quickstart.md)** — a drop-in UI that adds an AI detection button to any `textarea` or `contenteditable` in your web app. * **[Python SDK](https://sapling.ai/docs/sdk/Python/documentation.md)** — the `sapling-py` package wraps the endpoint as `client.aidetect(...)`. * **[Sapling MCP Server](https://sapling.ai/docs/mcp.md)** — exposes AI detection as tools to Claude and other MCP-compatible assistants: `sapling_ai_detect` for text and `sapling_ai_detect_url` for a web page. * **[Sapling's AI content detector](https://sapling.ai/ai-content-detector?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs)** — the hosted web app, useful for spot checks and for comparing against your own integration. If you are still evaluating vendors, Sapling maintains a [comparison of the top AI detection APIs](https://sapling.ai/ai-detection-apis?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs). ## Frequently asked questions[​](#frequently-asked-questions "Direct link to Frequently asked questions") ### What is an AI detection API?[​](#what-is-an-ai-detection-api "Direct link to What is an AI detection API?") An AI detection API is an HTTP endpoint that takes text as input and returns a score estimating how likely that text was generated by a large language model rather than written by a person. Sapling's AI detector API returns a document-level score from 0 to 1, plus optional per-sentence and per-token scores so you can see which passages drive the result. ### Which AI models can the detector identify?[​](#which-ai-models-can-the-detector-identify "Direct link to Which AI models can the detector identify?") The detector is trained across LLMs from different vendors, including OpenAI's GPT family, Google's Gemini models, Anthropic's Claude models, and the open-release Llama and Mistral models. It does not name which model produced the text; it returns the probability that the text is AI-generated. ### How much text does the AI detector API need?[​](#how-much-text-does-the-ai-detector-api-need "Direct link to How much text does the AI detector API need?") At least 300 characters is recommended. Shorter inputs carry much less signal, so scores on them are less reliable, and results on text under 500 characters come back with [`low_confidence`](#response-parameters) set to `true`. A single request accepts up to 200,000 characters. ### How accurate is AI detection?[​](#how-accurate-is-ai-detection "Direct link to How accurate is AI detection?") Every AI detection system has false positives and false negatives. Small edits to AI-generated text can keep it from being flagged, and human-written but rote text is sometimes misclassified as AI-generated. Treat the score as a signal to review rather than as proof, and [contact us](https://sapling.ai/contact?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs) to tune thresholds for your use case. ### Does the AI detector API support languages other than English?[​](#does-the-ai-detector-api-support-languages-other-than-english "Direct link to Does the AI detector API support languages other than English?") Yes: English, Spanish, Portuguese, French, German, Italian, Dutch and Japanese. The language is detected automatically and, unless you pin a `version`, text in the supported non-English languages is scored by a multilingual model (it has no Italian or Dutch training data yet, so it catches somewhat less AI-generated text in those two). Unless you pin a `version`, text in other languages is scored by the English model and is less reliable; the response's `language` field tells you which language was detected. See [Languages](#languages). ### Can I run AI detection on PDF or DOCX files?[​](#can-i-run-ai-detection-on-pdf-or-docx-files "Direct link to Can I run AI detection on PDF or DOCX files?") Yes. Upload the file to [`/api/v1/file-aidetect`](#checking-files). It accepts PDF, DOCX, PPTX, ODT, TXT, Markdown and HTML documents up to 30MB and scores the extracted text like a text request. ### Can I check a web page by URL?[​](#can-i-check-a-web-page-by-url "Direct link to Can I check a web page by URL?") Yes. Send the URL to [`/api/v1/url-aidetect`](#checking-web-pages). Sapling fetches the page, drops navigation, headers, footers and other boilerplate, and scores the main text like a text request. The response's `source` field reports the final URL, page title and characters scored. ### Does Sapling store the text I send to the AI detector?[​](#does-sapling-store-the-text-i-send-to-the-ai-detector "Direct link to Does Sapling store the text I send to the AI detector?") Not if you ask it not to. Add `"zero_retention": true` to a request and the text is scored and billed but not cached, usage is logged without the text, and nothing is saved to detector history. Teams with data retention disabled get this on every request. See [Data retention](#data-retention). ### Is there a free tier for the AI detection API?[​](#is-there-a-free-tier-for-the-ai-detection-api "Direct link to Is there a free tier for the AI detection API?") Yes. A new API key comes with a free quota of 50,000 characters per day and 250,000 characters per month, which is enough to evaluate the detector before subscribing. This quota is for trial keys only: subscribing raises a key's limits to 10 million characters per day and 25 million per month by default (see [Rate Limits](https://sapling.ai/docs/rate_limits.md)). Paid usage is billed per character with volume discounts — see [API Pricing](https://sapling.ai/docs/api/pricing). ### Does the API return a shareable report link?[​](#does-the-api-return-a-shareable-report-link "Direct link to Does the API return a shareable report link?") Yes, if you ask for one. Set `"create_report": true` on a single-text request sent with your private API key, and the response's `report` field holds a link to a shareable report saved to your AI detector history. Without `create_report`, API responses are returned as JSON with no shareable link. See [Shareable reports](#shareable-reports). ### Does the website AI detector use the same model as the API?[​](#does-the-website-ai-detector-use-the-same-model-as-the-api "Direct link to Does the website AI detector use the same model as the API?") Yes. The [sapling.ai AI detector](https://sapling.ai/ai-content-detector?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs) always uses the current default model: the same one an API request gets when it omits [`version`](#request-parameters), including the routing of supported non-English text to the multilingual model. Results differ only if an API request pins a different `version`. The website's per-check length limit depends on your plan; see [Pricing](https://sapling.ai/pricing?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs).   --- # Dictionary Add terms such as industry-specific jargon or product names to your dictionary so that Sapling will ignore these terms. The dictionary can also be managed in your web dashboard. ## Dictionary GET[​](#dictionary-get "Direct link to Dictionary GET") ### Request Parameters[​](#request-parameters "Direct link to Request Parameters") `https://api.sapling.ai/api/v1/dictionary?key=<key>` HTTP method: `GET` **\<key>**: String<br />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. * Python * JavaScript SDK ``` import requests key = '<api-key>' url = f'https://api.sapling.ai/api/v1/dictionary?key={key}' try: resp = requests.get(url) if 200 <= resp.status_code < 300: resp_json = resp.json() print('Dictionary entries: ', resp_json) else: print('Error: ', str(resp.status_code), resp.text) except Exception as e: print('Error: ', e) ``` ``` // npm install @saplingai/sapling-js import { Client } from '@saplingai/sapling-js/client'; const client = new Client('<api-key>'); const { data: entries } = await client.dictionaryEntries(); console.log('Dictionary entries: ', entries); ``` ### Response Parameters[​](#response-parameters "Direct link to Response Parameters") JSON array of dictionary entries, sorted by entry. Dictionary entry structure: ``` { "id": <str, UUID>, // Opaque id, used for deletion "entry": <str>, // Dictionary entry "case_sensitive": <bool>, // Whether or not entry is case sensitive "created_at": <str, date> // e.g. "Wed, 07 Oct 2020 21:15:47 GMT" "updated_at": <str, date> // e.g. "Wed, 07 Oct 2020 21:15:47 GMT" } ``` ## Dictionary POST[​](#dictionary-post "Direct link to Dictionary POST") ### Request Parameters[​](#request-parameters-1 "Direct link to Request Parameters") * Python * JavaScript SDK ``` import requests key = '<api-key>' url = 'https://api.sapling.ai/api/v1/dictionary' data = { 'key': key, 'entry': 'foobarbaz', } try: resp = requests.post(url, json=data) if 200 <= resp.status_code < 300: resp_json = resp.json() print('Entry: ', resp_json) else: print('Error: ', str(resp.status_code), resp.text) except Exception as e: print('Error: ', e) ``` ``` // npm install @saplingai/sapling-js import { Client } from '@saplingai/sapling-js/client'; const client = new Client('<api-key>'); const { data: entry } = await client.addDictionaryEntry('foobarbaz', { case_sensitive: true, // optional, defaults to true }); console.log('Entry: ', entry); ``` `https://api.sapling.ai/api/v1/dictionary` HTTP method: `POST` **key**: String<br />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. **entry**: String<br />Word to be added to the dictionary. **case\_sensitive**: Optional Boolean<br />Whether or not word is case-sensitive. Defaults to true. A duplicate entry (`"Entry already exists."`) or an entry containing more than two digits returns a 400. ### Response Parameters[​](#response-parameters-1 "Direct link to Response Parameters") JSON, the response contains the created dictionary entry Dictionary Entry Structure: ``` { "id": <str, UUID>, // Opaque id, used for deletion "entry": <str>, // Word in dictionary "case_sensitive": <bool>, // Whether or not entry is case sensitive "created_at": <str, date> // e.g. "Wed, 07 Oct 2020 21:15:47 GMT" "updated_at": <str, date> // e.g. "Wed, 07 Oct 2020 21:15:47 GMT" } ``` ## Dictionary DELETE[​](#dictionary-delete "Direct link to Dictionary DELETE") ### Request Parameters[​](#request-parameters-2 "Direct link to Request Parameters") * Python * JavaScript SDK ``` import requests key = '<api-key>' entry_id = '<DICTIONARY_ENTRY_ID>' url = f'https://api.sapling.ai/api/v1/dictionary/{entry_id}' data = { 'key': key, } try: resp = requests.delete(url, json=data) if 200 <= resp.status_code < 300: print('Success') else: print('Error: ', str(resp.status_code), resp.text) except Exception as e: print('Error: ', e) ``` ``` // npm install @saplingai/sapling-js import { Client } from '@saplingai/sapling-js/client'; const client = new Client('<api-key>'); await client.deleteDictionaryEntry('<DICTIONARY_ENTRY_ID>'); // 204, empty body ``` `https://api.sapling.ai/api/v1/dictionary/<dictionary_entry_id>` HTTP method: `DELETE` **\<dictionary\_entry\_id>**: String (UUID)<br />ID of dictionary entry. ### Response Parameters[​](#response-parameters-2 "Direct link to Response Parameters") Empty, HTTP status code 204. An id that is malformed or does not belong to your account returns a 400. --- # Grammar Check API Sapling's **grammar check API** takes a piece of text and returns grammar, spelling, and stylistic corrections as JSON **edits** — each with the sentence it belongs to, character offsets for the span to replace, a suggested replacement, and an error-type label. Because the endpoint returns edits rather than rewritten prose, your application decides how to render, filter, or apply them: inline underlines, a review queue, or a one-call auto-correct. The endpoint is customizable through a dictionary and custom mappings. The Postman "Sample Code" does not require any coding. | Grammar check API | At a glance | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | **Endpoint** | `POST https://api.sapling.ai/api/v1/edits` | | **Returns** | JSON `edits`: sentence, offsets, `replacement`, `error_type`; optional `applied_text` | | **Input limits** | Up to 100,000 characters per request; chunk above 4,000 to avoid timeouts | | **Languages** | 14 languages plus autodetect; US/UK/AU/CA English and Simplified/Traditional Chinese varieties | | **SDKs** | [Python](https://sapling.ai/docs/sdk/Python/documentation.md), [JavaScript](https://sapling.ai/docs/sdk/JavaScript/quickstart.md) | | **Pricing** | From $0.025 per 1,000 input characters, with volume discounts — see [API Pricing](https://sapling.ai/docs/api/pricing) | As of November 2024, the default model is Sapling's **Edits Large** model, which catches 27% more errors with fewer false positives than the earlier 2024 **Edits Small** system. In December 2024, another model upgrade with improvements to spelling and punctuation as well as handling of financial, legal, programming, and technical documents was released. Empirically, smaller models finetuned on customer-specific data outperform larger models. If you expect 100 million or more characters in usage volume per month, please [contact us](https://sapling.ai/contact?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs) for how we can develop custom models. Sapling also has existing prebuilt models for applications such as healthcare/medicine. [Demo](https://sapling.ai/grammar-check?utm_campaign=apidocs) ## Request Edits POST[​](#request-edits-post "Direct link to Request Edits POST") ### Sample Code[​](#sample-code "Direct link to Sample Code") * cURL * JavaScript * Python * Python SDK * JavaScript SDK * Postman ``` curl -X POST https://api.sapling.ai/api/v1/edits \ -H "Content-Type: application/json" \ -d '{"key":"<api-key>", "text":"Hi, how are you doing.", "session_id": "test session"}' ``` note You can also supply the API key via the `Authorization` header as a bearer token. If both the header and `key` field are sent, the `key` field takes precedence. info If you want to pretty-print JSON output so it's easier to read and you have Python installed, you can pipe command line outputs into `| python -m json.tool`. Example: `echo "{\"text\": \"Some text\"}" | python -m json.tool` info The results you get should match those from pasting the text on [https://sapling.ai/grammar-check](https://sapling.ai/grammar-check?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs) If the results do not match, check that you're not adding additional newlines or adding unnecessary character escaping (e.g. `\\r` or `\\n`) in your request. ``` import axios from 'axios'; async function run(text) { try { const response = await axios.post( 'https://api.sapling.ai/api/v1/edits', { "key": '<api-key>', // replace with your API key "session_id": 'test session', text, }, ); const {status, data} = response; console.log({status}); console.log(JSON.stringify(data, null, 4)); } catch (err) { const { msg } = err.response.data; console.log({err: msg}); } } run('Hi, how are you doing.'); // replace with the text you want to analyze ``` ``` import requests from pprint import pprint try: response = requests.post( "https://api.sapling.ai/api/v1/edits", json={ "key": "<api-key>", "text": "Hi, how are you doing.", "session_id": "test session" } ) if 200 <= response.status_code < 300: resp_json = response.json() edits = resp_json['edits'] pprint(edits) else: print("Error: ", str(response.status_code), response.text) except Exception as e: print("Error: ", e) ``` ``` # pip: python -m pip install sapling-py # uv: uv add sapling-py from sapling import SaplingClient from pprint import pprint api_key ='<api-key>' client = SaplingClient(api_key=api_key) edits = client.edits('Hi, how are you doing.', session_id="test_session") pprint(edits) ``` ``` // npm install @saplingai/sapling-js import { Client } from "@saplingai/sapling-js/client"; const apiKey = '<api-key>'; const client = new Client(apiKey); client .edits('Hi, how are you doing.') .then(function (response) { console.log(response.data); }) ``` [![Run in Postman](https://run.pstmn.io/button.svg)](https://app.getpostman.com/run-collection/20248607-4e2eafd4-e941-4d52-88a8-b4bb90c1d26a?action=collection%2Ffork\&collection-url=entityId%3D20248607-4e2eafd4-e941-4d52-88a8-b4bb90c1d26a%26entityType%3Dcollection%26workspaceId%3Dcea8d224-5c5e-4604-a8f6-46f228f46f78) * **[React full-stack integration](https://sapling.ai/docs/sdk/JavaScript/quickstart.md#fullstack-quickstart)** * **[HTML quickstart](https://sapling.ai/docs/sdk/HTML/quickstart.md)** ### Sample Response[​](#sample-response "Direct link to Sample Response") ``` { "edits": [ { "end": 22, "error_type": "R:PUNCT", "general_error_type": "Punctuation", "id": "4bb963a4-cc19-523e-9bb2-9e6b5a270bfc", "replacement": "doing?", "sentence": "Hi, how are you doing.", "sentence_start": 0, "start": 16 } ] } ``` ### Request Parameters[​](#request-parameters "Direct link to Request Parameters") `https://api.sapling.ai/api/v1/edits` HTTP method: `POST` To avoid client side time-outs, we recommend breaking text longer than 4000 characters into chunks/paragraphs before sending to Sapling. However, the endpoint will accept requests of up to 100,000 characters. Sapling offers a chunking endpoint you can use [here](https://sapling.ai/docs/api/chunk.md). This endpoint will return HTTP Status 400 with feedback in the `msg` field if the request is invalid—e.g if the key is missing or incorrect. The Edit API POST endpoint takes JSON parameters documented below: **key**: String<br />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<br />Text to process for edits. **session\_id**: Optional String<br />Unique name or UUID of document or portion of text that is being checked. If not provided, defaults to current date in `YYYYMMDD` format. **user\_id**: Optional String<br />Use this along with the accept and reject endpoints to allow different users to maintain individual preferences. **lang**: Optional String<br />Specifies language for grammar and spell checking. Supported Languages Specifies language for grammar and spell checking. Defaults to English, or the language specified in the dashboard. Supported languages: * `auto`: Autodetect language * `de`: German (Deutsch) * `el`: Greek (Ελληνικά) * `en`: English (US/UK/CA/AU) * `es`: Spanish (Español) * `fr`: French (Français) (`fr-fr` and `fr-ca` coming soon) * `it`: Italian (Italiano) * `jp`: Japanese (日本語) * `ko`: Korean (한국어) * `nl`: Dutch (Nederlands) * `pl`: Polish (Polski) * `pt`: Portuguese (Português) (`pt-pt` and `pt-br` coming soon) * `sv`: Swedish (Svenska) * `tl`: Tagalog * `zh`: Chinese (中文) With `lang` set to `auto`, text detected as a language that only the [Spellcheck](https://sapling.ai/docs/api/spellcheck.md) endpoint supports (for example Czech or Russian) gets spelling corrections but no grammar edits. The detected language code is returned in the response's `lang` field. **variety**: Optional String<br /> Supported Varieties This option is currently available for: English, Chinese (中文). Specifies regional English/Chinese spelling options. The setting defaults to the configuration in the Sapling dashboard. Supported varieties: English: * `us-variety`: American English * `gb-variety`: British English * `au-variety`: Australian English * `ca-variety`: Canadian English * `null-variety`: Don’t suggest any changes based on English variety Chinese (中文) * `s-variety`: Simplified Chinese * `t-variety`: Traditional Chinese **auto\_apply**: Optional Boolean<br />Default is false. If true, result with have extra field `applied_text` containing text with edits applied. **include\_error\_categories**: Optional Boolean<br />Default is `true`. Set to `false` to return corrections without `error_type`, `general_error_type`, or classifier-generated descriptions. This skips error classification, reducing classifier work and latency when you only need the edits. Edit `id`s, offsets, replacements, billing, and `auto_apply` behavior are unchanged. Advanced suggestions retain their own descriptions and are still excluded from `auto_apply`. The flag does not disable advanced suggestions or filter edits. ``` { "key": "YOUR_API_KEY", "text": "Lets get started!", "include_error_categories": false } ``` For SDK-format requests (`noformat: true`), `false` also overrides the classification requested by `sdk_extended` and `sdk_dictionary`; it does not generate their classifier-derived metadata. On-prem installations need `sapling_web_onprem:v137` or later; `v136` does not support this option. **neural\_spellcheck**: Optional Boolean<br />Default is false. If true, system will also apply more aggressive spellcheck to catch more spelling errors than the base system alone. **neural\_spellcheck\_mode**: Optional String<br />Default is `merge`. Controls how neural spellcheck edits are merged with grammar edits when `neural_spellcheck` is `true`. Possible values are: * `merge`: Removes spelling edits that overlap with grammar edits. Keeps all grammar edits and non-overlapping spelling edits. * `independent`: Removes spelling edits only if they are exactly identical to a grammar edit. Allows spelling and grammar edits to coexist even if they target the same location, as long as they suggest different changes. * `prefer_spelling`: Removes grammar edits that overlap with spelling edits. Keeps all spelling edits and non-overlapping grammar edits. **operations**: Optional List of Strings<br />Default is `[]`. Possible operations are described in the [Postprocess](https://sapling.ai/docs/api/postprocess.md) documentation. This will run postprocessing on outputs before returning results so you can enforce stricter capitalization, punctuation, and whitespace removal. **ignore\_edit\_types**: Optional List of Strings<br />Default is `[]`. Possible types are: * `articles_determiners`: Do not suggest `a` or `the` and do not suggest removing `a` or `the`. * `auxiliary_verbs`: Do not suggest [auxiliary verbs](https://en.wikipedia.org/wiki/Auxiliary_verb) such as `be`, `can`, or `will`. * `capitalization`: Do not suggest capitalizations. * `hyphens`: Do not suggest hyphens such as in `production ready` -> `production-ready`. * `punctuation`: Do not suggest terminating punctuation such as `.` or `?`. * `whitespace`: Do not suggest edits such as removing double spaces. **medical**: Optional Boolean<br />Default is false. If true, the backend will apply Sapling's medical dictionary. You can test the medical spellcheck system [here](https://sapling.ai/utilities/medical-spellcheck?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs). **team\_id**: Optional String (UUID or `user_only`)<br /> * If `team_id` is a UUID: apply user settings plus that specific team's settings (dictionary, custom mappings, etc.). Returns HTTP 403 if the team does not exist or the user is not a member of that team. * If `team_id` is `user_only`: apply only user settings (including user dictionary), with no team settings. * If omitted: apply user settings plus settings merged from all teams the user belongs to. ### Response Parameters[​](#response-parameters "Direct link to Response Parameters") The Edits POST endpoint returns JSON with parameters documented below. Every field other than `edits` is present only in the case described. **edits**: Array<br />Array of `Edit` objects for the text sent in the request, sorted by position in the text. Edit data structure: The category fields (`error_type` and `general_error_type`) below are omitted when `include_error_categories` is `false`. ``` { "id": <str, UUID>, // Opaque edit id, used to give feedback "sentence": <str>, // Unedited sentence "sentence_start": <int>, // Offset of sentence from start of text "start": <int>, // Offset of edit start relative to sentence "end": <int>, // Offset of edit end relative to sentence "replacement": <str>, // Suggested replacement "error_type": <str>, // Error type, see "Error Categories" "general_error_type": <str>, // See "Error Categories" } ``` **lang**: String<br />The detected language code. Returned only when the request sent `lang: "auto"`. **applied\_text**: String<br />The submitted text with every edit applied. Returned only when `auto_apply` is `true`. Advanced suggestions are never applied. **warning**: String<br />Returned with `applied_text` when the text contains HTML tags, which `auto_apply` does not account for. Strip the tags before sending if you need `applied_text`. **ignored-sentences**: Array<br />Sentences that were skipped without being checked. Each entry is `{"sentence": <str>, "reason": <str>}`, where `reason` is one of `SENTENCE_EXCEEDS_MAX_LENGTH`, `UNRECOGNIZED_CHARACTER_SET` or `INVALID_TOKENS_FOUND`. **degraded**: Object<br />Returned only on a partial result. Under heavy load a model stage may fail to process some sentences in time. The response still carries every edit that did complete, and each returned edit is valid. The affected sentences may be missing some or all of their edits. If the main grammar model failed for a sentence, that sentence is unchecked. If a later stage failed (for example `neural_spellcheck` or `operations` postprocessing), only that stage's corrections are missing: ``` { "edits": [ ... ], "degraded": { "sentences_degraded": 1, // Sentences that may be missing edits "sentences_total": 12, // Sentences in the request "reason": "model_timeout" // model_timeout | model_unavailable | model_error } } ``` `degraded` counts the affected sentences but doesn't identify them, so a sentence with few or no edits may be correct or may not have been fully checked. If you need a complete result, retry the whole request after a short backoff. When no sentence can be checked at all, the endpoint answers `503` with a `Retry-After` header instead of a partial result (see [Rate Limits & Errors](https://sapling.ai/docs/rate_limits.md#retrying-requests)). `degraded` is never present on a fully successful response. **usage**: Object<br />Returned only when the request sent `return_usage: true`; see [Per-Request Usage](https://sapling.ai/docs/api/api-reporting.md#per-request-usage). ### Tips[​](#tips "Direct link to Tips") * Check the status code of the result and any error logs. * Unless you're sending very large requests, the requests should rarely fail or time out. Retry `503`s after their `Retry-After` interval, and check for a `degraded` field if you need every sentence checked. See [retrying requests](https://sapling.ai/docs/rate_limits.md#retrying-requests) for a sample retry loop. * Use the accept/reject endpoints below if you would like Sapling to learn to stop making specific suggestions. * Follow the guidelines for [applying edits](https://sapling.ai/docs/api/applying-edits.md). ## Accept Edit POST[​](#accept-edit-post "Direct link to Accept Edit POST") Use this API endpoint to have Sapling adapt its system over time. Each suggested edit has an edit UUID. You can pass this information back to Sapling to indicate the edit suggestion was helpful. For each unique edit in each document, use the accept or reject API endpoint only once in total. ### Request Parameters[​](#request-parameters-1 "Direct link to Request Parameters") ``` import requests key = '<api-key>' edit_id = '<EDIT_ID>' url = f'https://api.sapling.ai/api/v1/edits/{edit_id}/accept' data = { 'key': key, 'session_id': 'Test Document UUID', } try: resp = requests.post(url, json=data) if 200 <= resp.status_code < 300: resp_json = resp.json() assert resp.status_code == 201 else: print("Error: ", str(resp.status_code), resp.text) except Exception as e: print("Error: ", e) ``` `https://api.sapling.ai/api/v1/edits/<edit_id>/accept` HTTP method: `POST` Note that the edit\_id is part of the URL. The Accept API POST endpoint takes a variety of JSON parameters documented below: **edit\_id**: String (UUID)<br />ID of an `Edit`. **key**: String<br />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. **session\_id**: Optional String<br />Unique name or UUID of document or portion of text that is being checked. If not provided, defaults to current date in `YYYYMMDD` format. **user\_id**: String<br />Pass this in to associate the accept event with a specific user. This allows different users to maintain different preferences. ### Response Parameters[​](#response-parameters-1 "Direct link to Response Parameters") HTTP status code 201. ## Reject Edit POST[​](#reject-edit-post "Direct link to Reject Edit POST") Use this API endpoint to have Sapling not recommend the same edit anymore. Each suggested edit has an edit UUID. You can pass this information back to Sapling to indicate the edit suggestion was not helpful. For each unique edit in each document, use the accept or reject API endpoint only once in total. ### Request Parameters[​](#request-parameters-2 "Direct link to Request Parameters") ``` import requests key = '<api-key>' edit_id = '<EDIT_ID>' url = f'https://api.sapling.ai/api/v1/edits/{edit_id}/reject' data = { 'key': key, 'session_id': 'Test Document UUID', } try: resp = requests.post(url, json=data) if 200 <= resp.status_code < 300: resp_json = resp.json() assert resp.status_code == 201 else: print("Error: ", str(resp.status_code), resp.text) except Exception as e: print("Error: ", e) ``` `https://api.sapling.ai/api/v1/edits/<edit_id>/reject` HTTP method: `POST` Note that `edit_id` is part of the URL. The Reject API POST endpoint has JSON parameters documented below: **edit\_id**: String (UUID)<br />ID of an `Edit`. **key**: String<br />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. **session\_id**: Optional String<br />Unique name or UUID of article or portion of text that is being checked. If not provided, defaults to current date in `YYYYMMDD` format. **user\_id**: String<br />Pass this in to associate the reject event with a specific user. This allows different users to maintain different preferences. ### Response Parameters[​](#response-parameters-2 "Direct link to Response Parameters") HTTP status code 201. Thresholding A single call to the reject endpoint for a specific `edit_hash` and user prevents that edit from being returned again (as long as data persistence is enabled). Similar edits are suppressed for 24 hours based on their similarity hashes, though behavior is less predictable for non-identical text. If you encounter unexpected behavior, please provide the exact text and requests sent and we can investigate further. Ignored edits are only available by querying the database due to the large number of possible events with even dozens of users. However, ignore events are used to determine when multiple users reject the same edit so that the unmodified string can be proposed as a dictionary entry; this does not affect whether an individual user sees the edit again. The thresholds for a dictionary entry default to `2` separate users ignoring an edit each at least once with a ratio of at least `2` ignores to `1` accept. ## Related grammar API endpoints[​](#related-grammar-api-endpoints "Direct link to Related grammar API endpoints") * **[Spellcheck](https://sapling.ai/docs/api/spellcheck.md)** — spelling only; faster and cheaper when you don't need grammar. * **[Applying Edits](https://sapling.ai/docs/api/applying-edits.md)** — how to apply the returned offsets correctly. * **[Error Categories](https://sapling.ai/docs/api/error-categories.md)** — what each `error_type` and `general_error_type` means. * **[Dictionary](https://sapling.ai/docs/api/dictionary.md)** and **[Custom Mappings](https://sapling.ai/docs/api/custom-mappings.md)** — exempt your own terms, or enforce your own corrections. * **[Advanced Edits](https://sapling.ai/docs/api/advanced-edits.md)** — simplification, confidence, and DEI suggestions beyond grammar. * **[JavaScript SDK](https://sapling.ai/docs/sdk/JavaScript/quickstart.md)** and **[Python SDK](https://sapling.ai/docs/sdk/Python/documentation.md)** — wrappers, plus a drop-in grammar-checking UI for web apps. * **[Comparison of grammar and spell check APIs](https://sapling.ai/docs/api/comparison.md)** — how Sapling's grammar API compares to the alternatives. ## Frequently asked questions[​](#frequently-asked-questions "Direct link to Frequently asked questions") ### What is a grammar check API?[​](#what-is-a-grammar-check-api "Direct link to What is a grammar check API?") A grammar check API is an HTTP endpoint that takes text as input and returns a list of suggested corrections rather than corrected prose. Sapling's grammar check API returns each correction as a JSON edit with the sentence it belongs to, character offsets for the span to replace, the suggested replacement, and an error-type label, so your application can render, filter, or apply corrections itself. ### Which languages does the grammar API support?[​](#which-languages-does-the-grammar-api-support "Direct link to Which languages does the grammar API support?") Fourteen languages — Chinese, Dutch, English, French, German, Greek, Italian, Japanese, Korean, Polish, Portuguese, Spanish, Swedish, and Tagalog — plus an `auto` setting that detects the language. English additionally supports American, British, Australian, and Canadian spelling varieties, and Chinese supports Simplified and Traditional. ### How much text can I check in one request?[​](#how-much-text-can-i-check-in-one-request "Direct link to How much text can I check in one request?") The endpoint accepts up to 100,000 characters per request, but we recommend splitting text longer than 4,000 characters into paragraphs or chunks to avoid client-side timeouts. The [chunking endpoint](https://sapling.ai/docs/api/chunk.md) can do that split for you. ### Does the grammar API return the corrected text?[​](#does-the-grammar-api-return-the-corrected-text "Direct link to Does the grammar API return the corrected text?") By default it returns edits, not rewritten text. Set `auto_apply` to `true` and the response includes an `applied_text` field with every edit already applied. You can also apply edits yourself from the offsets and replacements — see [Applying Edits](https://sapling.ai/docs/api/applying-edits.md). ### What is the difference between the grammar check API and the spellcheck endpoint?[​](#what-is-the-difference-between-the-grammar-check-api-and-the-spellcheck-endpoint "Direct link to What is the difference between the grammar check API and the spellcheck endpoint?") The edits endpoint covers grammar, spelling, and stylistic corrections. The separate [spellcheck endpoint](https://sapling.ai/docs/api/spellcheck.md) only checks spelling, which makes it faster and cheaper when your application doesn't need grammar or phrase-level suggestions. ### Can I stop the grammar API from suggesting certain edits?[​](#can-i-stop-the-grammar-api-from-suggesting-certain-edits "Direct link to Can I stop the grammar API from suggesting certain edits?") Yes, in three ways: `ignore_edit_types` suppresses whole categories such as capitalization, punctuation, or hyphens per request; a [dictionary](https://sapling.ai/docs/api/dictionary.md) exempts specific terms such as product names and jargon; and [custom mappings](https://sapling.ai/docs/api/custom-mappings.md) enforce your own preferred corrections. The reject endpoint above also teaches the system to stop making a specific suggestion over time. ### Is there a free tier for the grammar check API?[​](#is-there-a-free-tier-for-the-grammar-check-api "Direct link to Is there a free tier for the grammar check API?") Yes. A new API key comes with a free quota of 50,000 characters per day and 250,000 characters per month — see [Usage and Pricing](https://sapling.ai/docs/usage-pricing.md). Paid usage is billed per input character with volume discounts, starting at $0.025 per 1,000 characters. --- # Error Categories Error categories are returned as part of the Edits API. They provide additional classification and detail on the types of edits that Sapling suggests. These categories can be used for analysis, reporting, or coaching purposes. info Error types are currently trained on English. However, due to customer request, we've enabled the same model for German, Spanish, French, Italian, and Dutch. Please contact us if you need support for other languages. ## General Categories[​](#general-categories "Direct link to General Categories") * Grammar * Spelling * Punctuation * Fluency/Style * Other ## Subcategories[​](#subcategories "Direct link to Subcategories") Error subcategories are represented in 2 or or more components separated by a colon. The first part is a single letter, `M`, `R`, or `U`. This indicates if error is because something is missing (M), needs replacement (R), or is unnecessary (U). * `M`: Missing * `R`: Replacement * `U`: Unnecessary The second or optional third part of the error marks the error subcategory. If an error is a result of a Custom Mapping, an additional `:CUST` suffix is added. * `PART`: Particle * `PUNCT`: Punctuation * `ORTH`: Orthography * `SPELL`: Spelling * `SPACE`: Space * `WO`: Word Order * `MORPH`: Word Form * `ADV`: Adverb * `CONTR`: Contraction * `CONJ`: Conjunction * `DET`: Determiner * `DET:ART`: Article * `PREP`: Preposition * `PRON`: Pronoun * `VERB`: Verb * `VERB:FORM`: Verb Form * `VERB:TENSE`: Verb Tense * `VERB:SVA`: Subject-Verb Agreement * `VERB:INFL`: Verb Inflection * `ADJ`: Adjective * `ADJ:FORM`: Adjective Form * `NOUN`: Noun * `NOUN:POSS`: Noun Possessive * `NOUN:INFL`: Noun Inflection * `NOUN:NUM`: Noun Number * `OTHER`: Other ## Descriptions / Examples[​](#descriptions--examples "Direct link to Descriptions / Examples") For your application, it may be helpful to include brief descriptions and examples for the error category codes: * `M:PART`: Missing particle. **Example**: "Look the definition." is missing the particle 'up' as in "Look up the definition." * `M:PUNCT`: Missing punctuation. **Example**: "Its raining" is missing an apostrophe. * `M:CONJ`: Missing conjunction. **Example**: "I like tea coffee" is missing the conjunction 'and'. * `M:DET`: Missing determiner. **Example**: "There are just a exceptions" is missing the determiner 'few'. * `M:DET:ART`: Missing article. **Example**: "I saw elephant" is missing the article 'an'. * `M:PREP`: Missing preposition. **Example**: "I went the store" is missing the preposition 'to'. * `M:PRON`: Missing pronoun. **Example**: "She said that liked the movie." is missing the pronoun 'she'. * `M:VERB`: Missing verb. **Example**: "I to the park" is missing the verb 'went'. * `M:ADJ`: Missing adjective. **Example**: "It is a brand car" is missing the adjective 'new'. * `M:NOUN`: Missing noun. **Example**: "The is on the table" is missing the noun 'book'. * `M:NOUN:POSS`: Missing possessive noun. **Example**: "That is John car" is missing the possessive form "John's". * `M:OTHER`: Missing other elements. **Example**: NA * `R:PART`: Incorrect particle. **Example**: "He is interested for that" should have 'for' replaced with 'in'. * `R:PUNCT`: Incorrect punctuation. **Example**: "How are you today." should end with a question mark instead of a period. * `R:ORTH`: Incorrect orthography. **Example**: "I like readin" should have 'readin' spelled as 'reading'. * `R:SPELL`: Incorrect spelling. **Example**: "I like chokolate" should have 'chokolate' spelled as 'chocolate'. * `R:WO`: Incorrect word order. **Example**: "I the book read" should be rearranged to "I read the book." * `R:MORPH`: Incorrect word form. **Example**: "It is danger" should be "It is dangerous". * `R:ADV`: Incorrect adverb. **Example**: "She plays tennis good" should have 'good' replaced with 'well'. * `R:CONTR`: Incorrect contraction. **Example**: "I'm not have time" should have 'I'm not have' replaced with "I don't have." * `R:CONJ`: Incorrect conjunction. **Example**: "Neither sea or air" should have 'or' replaced with 'nor'. * `R:DET`: Incorrect determiner. **Example**: "I like this shoes" should instead be "I like these shoes." * `R:DET:ART`: Incorrect article. **Example**: "I saw a elephant" should have 'a' replaced with 'an'. * `R:PREP`: Incorrect preposition. **Example**: "I am in work" should have 'in' replaced with 'at'. * `R:PRON`: Incorrect pronoun. **Example**: "The group told us us will be there." should have 'us' replaced with 'they'. * `R:VERB:FORM`: Incorrect verb form. **Example**: "He is walk to the store" should have 'walk' replaced with 'walking'. * `R:VERB:TENSE`: Incorrect verb tense. **Example**: "I go to the park yesterday" should have 'go' replaced with 'went'. * `R:VERB:SVA`: Incorrect subject-verb agreement. **Example**: "They likes ice cream" should have 'likes' replaced with 'like'. * `R:ADJ:FORM`: Incorrect adjective form. **Example**: "She is much happiest than me" should have 'happiest' replaced with 'happier'. * `R:NOUN:INFL`: Incorrect noun inflection. **Example**: "I have two childs" should have 'childs' replaced with 'children'. * `R:NOUN:NUM`: Incorrect noun number. **Example**: "I have a cars" should have 'a cars' replaced with 'a car'. * `R:OTHER`: Replacement of phrase not easily categorized into a grammatical category. **Example**: In "He go store", 'go' should be replaced with 'goes to the'. * `U:PART`: Unnecessary particle. **Example**: "She knocked out on the door." has an unnecessary 'out'. * `U:PUNCT`: Unnecessary punctuation. **Example**: "It's my friend's, bag" has an unnecessary comma. * `U:ADV`: Unnecessary adverb. **Example**: "She ran fast fast" has an unnecessary 'fast'. * `U:CONTR`: Unnecessary contraction. **Example**: "I'm am ready." has an unnecessary contraction "'m". * `U:CONJ`: Unnecessary conjunction. **Example**: "I like tea and and coffee" has an unnecessary 'and'. * `U:DET`: Unnecessary determiner. **Example**: "I saw your your cat" has an unnecessary 'your'. * `U:DET:ART`: Unnecessary article. **Example**: "I saw the an elephant" has an unnecessary 'an'. * `U:PREP`: Unnecessary preposition. **Example**: "I am at in home" has an unnecessary 'in'. * `U:PRON`: Unnecessary pronoun. **Example**: "He he likes dogs" has an unnecessary 'he'. * `U:VERB`: Unnecessary verb. **Example**: "I went go to the park yesterday" has an unnecessary 'go'. * `U:ADJ`: Unnecessary adjective. **Example**: "She is more happier than me" has an unnecessary 'more'. * `U:NOUN`: Unnecessary noun. **Example**: "The dog dog is on the table" has an unnecessary 'dog'. * `U:NOUN:POSS`: Unnecessary possessive noun. **Example**: "That is John's 's car" has an unnecessary 's'. * `U:OTHER`: Unnecessary text. **Example**: NA As a more complete example, for the edit mapping `The cat in hat` to `The cat in the hat`, the error type would be `M:DET` (missing determiner, in this case: `the`). ``` { "edits": [ { "general_error_type": "Grammar", "error_type": "M:DET:ART", "id": "c73ce00c-7f3f-5759-b3be-4630cc81afed", "sentence_start": 0, "sentence": "The cat in hat", "replacement": "the hat", "start": 11, "end": 14 } ] } ``` --- # Benchmarking & Evaluation How do you determine whether one NLP system that suggests edits or completions is better than another system? One way is to try both out on a dozen hand-picked sentences and see which system catches the most errors or makes the most accurate predictions. However, we'll argue that this is not a good way to perform an evaluation. There are a couple key components to evaluating an NLP system. The first is the evaluation dataset. How is the dataset gathered and annotated? How much data is there, and what are the sources? The second is the metric chosen. Given the dataset, there is usually a standard metric used to evaluate the performance of systems, so the metric is more straightforward. ## Data[​](#data "Direct link to Data") For evaluation, there needs to be both sufficient **quality** as well as **quantity** of data. Sports Analogies Why are both quality and quantity of data so important? Let's take two analogies from sports. 🏃 If a coach was choosing runners for the Olympic 10,000m team, she would at the very least time the runners over the 10,000m distance. Imagine she instead chose to measure their 100m dash times. The 100m results (where reaction time and power are critical) may not even correlate with 10,000m performance (where endurance is critical) at any competitive level. Similarly, NLP systems should be evaluated on data that tests the right metric and is representative of the real-world, production setting. 🏀 When evaluating whether a basketball player is good or not, we would never simply evaluate them on a single 12-minute quarter. Instead, we need to evaluate them over a period of a dozen or so games, maybe even over half of an 82-game season. Evaluating them based on a single quarter is akin to evaluating an NLP system off of only a dozen or so examples — there is too much variance with such a small sample size. Take grammar and spelling as an example. It's tempting to generate obvious, glaring errors such as "hy howz it goying". *However, these types of errors are rarely seen in practice and favor simple dictionary lookups. High-quality data should come from errors made in real-world usage.* This will capture mistakes that stem from keyboard errors, phonetic mixups, and users where English (or whatever language you're checking) is not their first language. Once you gather such data, the difference between these errors vs. intentional errors will be stark. As another example, consider autocorrect. Autocorrect must have extremely low rates of false positives, or it becomes too annoying and users will turn it off. It's much better to have autocorrect that's accurate but doesn't make many corrections than have it be inaccurate but make many corrections. In terms of data quantity, we suggest at least a hundred sentences containing at last a hundred errors. This will ensure the metrics we describe below are statistically significant. ## Automated Metrics[​](#automated-metrics "Direct link to Automated Metrics") Automated metrics can be computed immediately after running a system on the evaluation data. One example is F-score, which looks at the true positives, false positives, false negatives, and true negatives of a system. We would like true positives to be as high as possible while avoiding too many false positives. There are also probabilistic metrics that are frequently used — contact us to learn more. ## Usage-Based and Qualitative Metrics[​](#usage-based-and-qualitative-metrics "Direct link to Usage-Based and Qualitative Metrics") Metrics based on usage and user feedback take longer to collect but are ultimately what we wish to measure. Again taking grammar checking as an example, usage-based metrics may include number of accepted suggestions, number of ignored suggestions, and total number of suggestions made. Further downstream, we may wish to measure language quality, time saved, and impact on the customer (e.g., by running an A/B test). --- # Data Extraction API The extract endpoint pulls **fields you name** out of unstructured text — invoices, emails, resumes, support tickets, contracts — and returns them as typed JSON. There is no schema to train and no template to maintain: describe the fields in the request and the endpoint fills in the ones the document actually states. Nearly every value is **grounded**: the model has to quote the span of the text a value came from, and the server verifies that quote really occurs in the document. Anything it cannot point at is reported missing instead of guessed. The one exception is `boolean` fields, whose true/false answers are judgments about the text rather than spans in it and so are not span-verified — see [Tips](#tips). Values are also coerced to the type you declare, so `total` comes back as `1299.0` rather than `"$1,299.00"`, and dates come back as `YYYY-MM-DD`. Where the [sentiment](https://sapling.ai/docs/api/sentiment.md) and [tone](https://sapling.ai/docs/api/tone.md) endpoints answer "what kind of text is this?", extract answers "what is in it?". For a value that is nowhere stated in the document and has to be inferred or condensed, use [summarize](https://sapling.ai/docs/api/summarize.md) instead — see [Tips](#tips). [Try it out](https://sapling.ai/utilities/data-extractor?utm_campaign=apidocs) ## Sample Code[​](#sample-code "Direct link to Sample Code") * cURL * JavaScript * Python * Python (Sapling client) ``` curl -X POST https://api.sapling.ai/api/v1/extract \ -H "Content-Type: application/json" \ -d '{"key":"<api-key>", "text":"Invoice INV-1042 for Acme Corp, total $1,299.00, due March 5, 2026.", "fields": ["invoice_number", {"name": "total", "type": "number", "description": "Amount due", "required": true}, {"name": "due_date", "type": "date"}, "purchase_order"], "context": "A vendor invoice"}' ``` ``` import axios from 'axios'; async function run(text) { try { const response = await axios.post( 'https://api.sapling.ai/api/v1/extract', { key: '<api-key>', text, fields: [ 'invoice_number', {name: 'total', type: 'number', description: 'Amount due', required: true}, {name: 'due_date', type: 'date'}, 'purchase_order', ], context: 'A vendor invoice', }, ); 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('Invoice INV-1042 for Acme Corp, total $1,299.00, due March 5, 2026.'); ``` ``` import requests from pprint import pprint response = requests.post( "https://api.sapling.ai/api/v1/extract", json={ "key": "<api-key>", "text": "Invoice INV-1042 for Acme Corp, total $1,299.00, due March 5, 2026.", "fields": [ "invoice_number", {"name": "total", "type": "number", "description": "Amount due", "required": True}, {"name": "due_date", "type": "date"}, "purchase_order", ], "context": "A vendor invoice", } ) 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) text = 'Invoice INV-1042 for Acme Corp, total $1,299.00, due March 5, 2026.' fields = [ 'invoice_number', {'name': 'total', 'type': 'number', 'description': 'Amount due', 'required': True}, {'name': 'due_date', 'type': 'date'}, 'purchase_order', ] result = client.extract(text, fields, context='A vendor invoice') # `data` is a plain {name: value} map with an entry for every requested field print(result['data']['invoice_number'], result['data']['total']) # `fields` keeps the request order and carries the quoted evidence span for entry in result['fields']: if entry['found']: print(f"{entry['name']}: {entry['value']} <- {entry['evidence']}") print('Not stated in the text:', result['missing']) # ['purchase_order'] ``` ## Sample Response[​](#sample-response "Direct link to Sample Response") ``` { "data": { "invoice_number": "INV-1042", "total": 1299.0, "due_date": "2026-03-05", "purchase_order": null }, "fields": [ {"name": "invoice_number", "type": "string", "value": "INV-1042", "evidence": "Invoice INV-1042", "found": true}, {"name": "total", "type": "number", "value": 1299.0, "evidence": "total $1,299.00", "found": true}, {"name": "due_date", "type": "date", "value": "2026-03-05", "evidence": "due March 5, 2026", "found": true}, {"name": "purchase_order", "type": "string", "value": null, "evidence": "", "found": false} ], "missing": ["purchase_order"] } ``` A document that yields nothing is still a successful `200`: `data` is all `null`, every `found` is `false`, and `missing` lists all of your field names. ## Batch Requests[​](#batch-requests "Direct link to Batch Requests") To run the same extraction over many short documents — a folder of receipts, a queue of form submissions — in one request, send `texts` (a list of 1–10 strings) instead of `text`. The same `fields` and `context` 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/extract \ -H "Content-Type: application/json" \ -d '{"key":"<api-key>", "texts": ["Invoice INV-1042 for Acme Corp.", "Invoice INV-2077 for Globex."], "fields": ["invoice_number", "customer_name"]}' ``` The batch response is `{"results": [...]}` with one entry per input, in input order; each entry has exactly the single-response shape above (`data`, `fields`, `missing`): ``` { "results": [ { "data": {"invoice_number": "INV-1042", "customer_name": "Acme Corp"}, "fields": [ {"name": "invoice_number", "type": "string", "value": "INV-1042", "evidence": "Invoice INV-1042", "found": true}, {"name": "customer_name", "type": "string", "value": "Acme Corp", "evidence": "Acme Corp", "found": true} ], "missing": [] }, { "data": {"invoice_number": "INV-2077", "customer_name": "Globex"}, "fields": [ {"name": "invoice_number", "type": "string", "value": "INV-2077", "evidence": "Invoice INV-2077", "found": true}, {"name": "customer_name", "type": "string", "value": "Globex", "evidence": "Globex", "found": true} ], "missing": [] } ] } ``` 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 extract, 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. ## File Uploads[​](#file-uploads "Direct link to File Uploads") Invoices, resumes and contracts usually arrive as files. Instead of extracting their text yourself, you can upload the document to the file endpoint. It reads the document's text and runs the same extraction as `text`. **POST** to `https://api.sapling.ai/api/v1/file-extract` as `multipart/form-data`. * **`file`** (required): the document. The format is chosen by file extension: `.pdf`, `.docx`, `.pptx`, `.odt`, `.txt`, `.md`, `.html` or `.htm`. Uploads are limited to 30MB; a larger file returns `413`. * **`jsonParams`** (required): a JSON object with the options below, sent as a file part or as a plain form field. `fields` and `context` take the same values as on `/api/v1/extract`: * `fields`: required. * `key`: optional when you send the key as a bearer token in the `Authorization` header. * `context`: optional. * `return_usage`: optional boolean; `true` adds the character count submitted for metering to the response. It is counted before repeat-request deduplication, so it can exceed what you are billed (see [Per-Request Usage](https://sapling.ai/docs/api/api-reporting.md#per-request-usage)). Any other option returns a `400`, so a misspelled `"feilds"` is never read as "no fields". ``` curl -X POST https://api.sapling.ai/api/v1/file-extract \ -F "file=@invoice.pdf" \ -F 'jsonParams={"key":"<api-key>", "fields": ["invoice_number", {"name": "total", "type": "number"}, {"name": "due_date", "type": "date"}], "context": "A vendor invoice"}' ``` ``` import json import requests params = { "key": "<api-key>", "fields": ["invoice_number", {"name": "total", "type": "number"}, {"name": "due_date", "type": "date"}], "context": "A vendor invoice", } with open("invoice.pdf", "rb") as f: response = requests.post( "https://api.sapling.ai/api/v1/file-extract", files={"file": f}, data={"jsonParams": json.dumps(params)}, ) print(response.status_code, response.json()) ``` The response has the single-request shape (`data`, `fields`, `missing`) plus a `document` object: ``` { "data": {"invoice_number": "INV-1042", "total": 1299.0, "due_date": "2026-03-05"}, "fields": [...], "missing": [], "document": { "file_type": "pdf", "characters": 2310, "characters_used": 2310, "truncated": false } } ``` * **`file_type`**: the extension the document was read as. * **`characters`**: the length of the text extracted from the file. * **`characters_used`**: how much of that text was read for extraction. This is also what is billed. * **`truncated`**: `true` when the text was longer than 10,000 characters (the same cap as `text`). In that case only the first 10,000 characters are read, cut back to a word boundary. Fields that only appear later in the document are reported missing. To read a later section, extract its text yourself and send it as `text`. Billing, caching and the `fields` rules are the same as for `text`. Problems with the file itself return `{"error": "<reason>"}`. A `400` here means one of these: * the `file` part is missing * the extension is unsupported * the document is empty, encrypted, corrupt or has no extractable text Problems with `jsonParams` return `{"msg": "..."}`. Upload requests are limited to 60 per minute per account; the limit is shared with the other upload endpoints. ## Request Parameters[​](#request-parameters "Direct link to Request Parameters") **POST** to `https://api.sapling.ai/api/v1/extract` **key**: String<br />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<br />The document to extract from, up to 10,000 characters (measured on the raw input, before any HTML is stripped). Plain text or HTML — tags are stripped server-side. Text that is empty after stripping returns a `400`. The text is treated as untrusted data: instructions inside it are ignored and only its content is extracted from. Provide exactly one of `text` and `texts`. **texts**: List\[String]<br />Batch form (see [Batch Requests](#batch-requests)): 1–10 documents to extract from in one request, each treated exactly like `text` above, with a combined length of up to 10,000 characters. An item that is empty after stripping returns a `400` naming its index. The response becomes `{"results": [...]}`, one entry per item in input order. **fields**: List\[String | Object]<br />The fields to look for: between 1 and 20 entries. Each entry is either a string (the field name) or an object with the keys below; any other key returns a `400`. Names are at most 50 characters, non-empty, and case-insensitively unique across the list. * `name` String — the field name, echoed back as the key in `data`. * `type` String, optional, defaults to `string` — one of `string`, `number`, `integer`, `boolean`, `date` or `list`. Unknown types return a `400`. * `description` String, optional — up to 200 characters saying what this field means; the cheapest way to fix a field that is being read wrongly. * `required` Boolean, optional, defaults to false — a hint that the field is expected to be present. It never forces a value: a required field that the text does not state is still reported in `missing` rather than guessed. **context**: String, optional<br />Up to 500 characters describing what the document is and how to read ambiguous fields, for example `"A vendor invoice; total means the amount due including tax"`. Guidance only — it can never add a field outside `fields`. ## Response Parameters[​](#response-parameters "Direct link to Response Parameters") **data**: Object<br />A convenience `{field name: value}` map. **Every requested field is present**, with `null` where the document did not state it, so you can index it without checking for missing keys. **fields**: List\[Object]<br />One entry per requested field, **in the order you requested them**. Each has: * `name`: the field name as submitted. * `type`: the type the value was coerced to (`string` when you did not declare one). * `value`: the extracted value, or `null` when not found. * `evidence`: the verbatim span of `text` the value was taken from, or `""` when not found. Useful for highlighting the source in your own UI and for spot-checking results. * `found`: whether a value was extracted. **missing**: List\[String]<br />The names of the fields the document did not yield, in request order. Empty when everything was found. ### Types[​](#types "Direct link to Types") | `type` | Value in the response | | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `string` | The extracted text. | | `number` | A float. Currency symbols, thousands separators and trailing units are stripped (`"$1,299.00"` → `1299.0`). | | `integer` | A whole number; a value with a fractional part is reported missing rather than rounded. | | `boolean` | `true` or `false`. | | `date` | Always `YYYY-MM-DD`. `"March 5, 2026"`, `"5 March 2026"` and ISO timestamps all normalize; ambiguous numeric forms like `03/04/2026` are deliberately **not** guessed. | | `list` | A list of strings, deduplicated, at most 20 items. | A value that will not convert to the declared type is reported missing rather than returned in the wrong type, so you never have to type-check `data` yourself. ## Errors[​](#errors "Direct link to Errors") | Status | Meaning | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `400` | Validation error — missing or oversized `text`, text empty after stripping, both or neither of `text`/`texts` provided, an invalid or over-length `texts` batch, `context` over 500 characters, or invalid `fields` (empty, more than 20, a duplicate or oversized name, an unknown `type`, or an unrecognized key inside a field object). The body is `{"msg": "..."}`. | | `401` / `403` | Missing or invalid API key. | | `413` | `/api/v1/file-extract` only: the upload is over 30MB. The body is `{"error": "..."}`. | | `429` | Rate limit exceeded or key over capacity. Each call draws at least 100 tokens from the burst bucket. | | `502` | `{"msg": "Unexpected error extracting fields."}` — the extraction model failed; safe to retry. Not billed and not cached. | ## Tips[​](#tips "Direct link to Tips") * **Describe the ambiguous fields.** A bare name works for `invoice_number`, but a one-line `description` is what separates `total` (amount due, including tax) from `subtotal`, or `start_date` from `signed_date`. This is usually the first thing to reach for when a field comes back wrong. * **Declare types.** `{"name": "amount", "type": "number"}` gets you a float you can sum, and `"type": "date"` gets you an ISO date you can sort — no parsing on your side, and values that do not convert are reported missing rather than returned in a shape your code did not expect. * **Use `list` for repeated items.** Line items, attendees, skills, ticket tags — one `list` field beats `item_1`, `item_2`, `item_3`, and each item is grounded in the text individually. * **Extraction is not inference.** Values must be stated in the document; the grounding check discards anything the model cannot quote. `boolean` fields are the exception (a true/false answer such as `{"name": "is_urgent", "type": "boolean"}` is a judgment about the text rather than a span in it). For a value that has to be inferred or condensed rather than quoted, use [summarize](https://sapling.ai/docs/api/summarize.md). * **Treat `missing` as normal.** Documents vary, and an all-`null` result is a successful response, not an error. Branch on `found` (or on `missing`) rather than assuming every field will be filled, and use `required` to mark the fields whose absence should raise a flag in your own workflow. * **Show the evidence.** When a person reviews extractions, rendering `evidence` next to each value makes verification a glance instead of a re-read of the whole document. * Results are cached on Sapling's side for about three days keyed on the text, fields and context, so re-running an identical request is fast — but the same document against a different field set is separate work.   --- # Files In addition to plain text, Sapling also supports PDF and DOCX processing with its PDF-to-text and DOCX-to-text endpoints. Note that these endpoints do not use just plain JSON as with other Sapling endpoints since the files must be submitted as binary data. tip If you only need the text in order to run another endpoint on it, you can upload the document to that endpoint directly: * [`/api/v1/file-aidetect`](https://sapling.ai/docs/api/detector.md#checking-files) scores a document for AI-generated text. * [`/api/v1/file-extract`](https://sapling.ai/docs/api/extract.md#file-uploads) pulls typed fields out of one. * [`/api/v1/file-summarize`](https://sapling.ai/docs/api/summarize.md#file-uploads) summarizes one. All three accept PDF, DOCX, PPTX, ODT, TXT, Markdown and HTML files up to 30MB. The JavaScript SDK (`@saplingai/sapling-js/client`) provides `fileAidetect()`, `fileExtract()`, and `fileSummarize()` upload helpers. See the [AI Detector](https://sapling.ai/docs/api/detector.md#checking-files), [Extraction](https://sapling.ai/docs/api/extract.md#file-uploads), and [Summarization](https://sapling.ai/docs/api/summarize.md#file-uploads) references. The PDF-to-text and DOCX-to-text endpoints below accept multipart form requests directly, as shown in the Python and JavaScript examples. ## PDF to Text POST[​](#pdf-to-text-post "Direct link to PDF to Text POST") `https://api.sapling.ai/api/v1/ingest/pdf_to_text` HTTP method: `POST` ### Request Parameters[​](#request-parameters "Direct link to Request Parameters") Please refer to the code examples below. You must submit the file in a `file` field as well as a `jsonParams` field that has the API key. * Python * JavaScript ``` import json import requests def process_pdf(file_path, json_params): url = 'https://api.sapling.ai/api/v1/ingest/pdf_to_text' files = { 'file': open(file_path, 'rb'), 'jsonParams': ('jsonParams', json.dumps(json_params), 'application/json') } response = requests.post(url, files=files) if response.status_code == 200: print(response.json()) else: print(f'Failed, status code: {response.status_code}') print(response.text) if __name__ == '__main__': pdf_file_path = 'path_to_your_pdf_file.pdf' json_data = { 'key': '<api-key>', # Do not expose private key in production } process_pdf(pdf_file_path, json_data) ``` ``` // Assumes there is a <input type="file" id="pdfFile" accept=".pdf"> let fileInput = document.getElementById('pdfFile'); let file = fileInput.files[0]; if (!file) { // Error } let formData = new FormData(); formData.append('file', file); const jsonParams = JSON.stringify({ key: '<api-key>', // Do not expose private key in production }); formData.append('jsonParams', new Blob([jsonParams], { type: 'application/json' })); const response = await fetch('https://api.sapling.ai/api/v1/ingest/pdf_to_text', { method: 'POST', credentials: 'include', body: formData }) console.log(response.json()); ``` ### Response Parameters[​](#response-parameters "Direct link to Response Parameters") ``` { "text": "[EXTRACTED TEXT]" } ``` ## DOCX to Text POST[​](#docx-to-text-post "Direct link to DOCX to Text POST") `https://api.sapling.ai/api/v1/ingest/docx_to_text` HTTP method: `POST` ### Request Parameters[​](#request-parameters-1 "Direct link to Request Parameters") Please refer to the code examples below. You must submit the file in a `file` field as well as a `jsonParams` field that has the API key. * Python * JavaScript ``` import json import requests def process_docx(file_path, json_params): url = 'https://api.sapling.ai/api/v1/ingest/docx_to_text' files = { 'file': open(file_path, 'rb'), 'jsonParams': ('jsonParams', json.dumps(json_params), 'application/json') } response = requests.post(url, files=files) if response.status_code == 200: print(response.json()) else: print(f'Failed, status code: {response.status_code}') print(response.text) if __name__ == '__main__': docx_file_path = 'path_to_your_docx_file.docx' json_data = { 'key': '<api-key>', # Do not expose private key in production } process_docx(docx_file_path, json_data) ``` ``` // Assumes there is a <input type="file" id="docxFile" accept=".docx"> let fileInput = document.getElementById('docxFile'); let file = fileInput.files[0]; if (!file) { // Error } let formData = new FormData(); formData.append('file', file); const jsonParams = JSON.stringify({ key: '<api-key>', // Do not expose private key in production }); formData.append('jsonParams', new Blob([jsonParams], { type: 'application/json' })); const response = await fetch('https://api.sapling.ai/api/v1/ingest/docx_to_text', { method: 'POST', credentials: 'include', body: formData }) console.log(response.json()); ``` ### Response Parameters[​](#response-parameters-1 "Direct link to Response Parameters") ``` { "text": "[EXTRACTED TEXT]" } ``` ## Text extraction behavior[​](#text-extraction-behavior "Direct link to Text extraction behavior") The PDF-to-text endpoint extracts embedded text; it does not perform OCR on scans or image-only PDFs. PDFs with no extractable text and files that require a password return HTTP `400`; permission-restricted PDFs that can be read without a password can be processed. Some layout line breaks and split words are normalized. If an individual PDF page cannot be parsed, it may be skipped while the rest of the document is returned. The DOCX-to-text endpoint extracts paragraphs and tables in document order, including text inside hyperlinks. Table cells are separated by tabs, and nested tables are flattened. Merged cells are not repeated. File extensions are matched case-insensitively (for example `.PDF` and `.DOCX`). Invalid or unreadable documents return HTTP `400`. --- # Handling Fragments Sometimes the text in requests may not be full sentences but instead incomplete sentences or sentence fragments. Because Sapling's models are trained to work with text produced as users are typing, it should do a reasonable job of not making recommendations for incomplete words. However, you may still find that it suggests capitalization and terminating punctuation more frequently than you'd like. To handle this, Sapling offers the `ignore_edit_types` argument in its [edits endpoint](https://sapling.ai/docs/api/edits-overview.md). This allows you to ignore specific edit types, including capitalization and punctuation. Let us know if you need additional support for fragments.   --- # Guardrails Despite their amazing capabilities, LLMs can often behave in undesired ways, such as by generating offensive or toxic text, or by revealing sensitive information. To use an LLM in production, guardrails should be put in place to avoid such behavior. Sapling's endpoints are designed and trained from the ground up to avoid the need for guardrails. However, with the vast savings from using pre-trained models (which are then finetuned for specific tasks), training models with cleaned data from scratch is usually not feasible for most teams. Here we describe some offerings where Sapling can assist with avoiding the generation of undesirable text. ## Content Safety[​](#content-safety "Direct link to Content Safety") Sapling's [content safety endpoint](https://sapling.ai/docs/api/safety.md) scores text on seven moderation categories — toxicity, profanity, harassment, hate speech, self-harm, sexual content, and violence — and is the recommended way to screen both user inputs and LLM outputs. ## Profanity[​](#profanity "Direct link to Profanity") Profanity refers to swear words or other offensive words. Sapling offers a [profanity filter](https://sapling.ai/utilities/profanity?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs) utility for token-level flagging; for graded scoring use the `profanity` category of the [content safety endpoint](https://sapling.ai/docs/api/safety.md). ## NSFW[​](#nsfw "Direct link to NSFW") NSFW stands for "not safe for work" or alternatively "not suitable for work". This can often refer to adult content that should not be viewed at work or in public spaces. The `sexual` category of the [content safety endpoint](https://sapling.ai/docs/api/safety.md) covers most NSFW content; contact us for use cases it does not cover. ## PII[​](#pii "Direct link to PII") PII stands for Personally Identifiable Information. PII can include names, addresses, emails, phone numbers, and other information that allows the identity of an individual to be inferred. This is especially important to safeguard in the case where PII can be tied to other sensitive information, such as medical information about a patient. Sapling's [PII detection & redaction endpoint](https://sapling.ai/docs/api/pii.md) finds emails, phone numbers, US SSNs, credit card numbers, IP addresses, IBANs and US bank routing numbers with exact character offsets, and can return a redacted copy of the text with `[TYPE]` placeholders — use it to scrub prompts before they reach an LLM and transcripts before they are stored. Names and street addresses are not covered by that endpoint; contact us if you need them. ## Tone[​](#tone "Direct link to Tone") Oftentimes businesses may want user-facing messaging to have a specific tone. Sapling detects over [25 tones](https://sapling.ai/docs/api/tone) grouped into those with positive and negative [sentiment](https://sapling.ai/docs/api/sentiment/). Test out our [tone](https://sapling.ai/utilities/sentiment?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs) and [sentiment](https://sapling.ai/utilities/tone?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs) utilities. ## Secrets[​](#secrets "Direct link to Secrets") Secrets refers to programming artifacts such as API keys, encryption keys (such as SSH keys), access tokens, and passwords. These can result in major security breaches if exposed. ## Numerical Expressions[​](#numerical-expressions "Direct link to Numerical Expressions") Numerical expressions can include data such as SSNs, phone numbers, and financial data. There is significant overlap between numerical expressions and the [PII endpoint](https://sapling.ai/docs/api/pii.md) (which validates SSNs, card numbers, IBANs and routing numbers by checksum); you may wish to use one or the other or both. ## Prompt Injection[​](#prompt-injection "Direct link to Prompt Injection") Based on the SQL injection vulnerability, prompt injection is a way for malicious users to get an LLM to generate unintended outputs such as the internal system prompt, in some cases even allowing for data exfiltration. ## Gibberish[​](#gibberish "Direct link to Gibberish") While this is becoming less common, occasionally highly unexpected inputs or a poor decoding process can result in gibberish generated by an AI system. --- # Inclusive Language API The Inclusive Language endpoint finds non-inclusive or biased wording in a text: gendered defaults ("chairman", "he" for an unknown person), ableist idioms ("crazy", "tone-deaf"), ageist framing ("digital natives"), outdated or loaded terms for groups, and classist or appearance-based language. Each issue comes back with a category, a severity, a short explanation, a neutral replacement and — for all but the rare passage that can't be located — its exact character offsets, so you can underline it in an editor or rewrite it automatically. The check targets **wording, not topics**. A text that discusses discrimination, or quotes a term in order to explain or criticize it, is not flagged. Every issue is grounded: the model quotes the passage it flagged, and if that passage doesn't occur in your text, the issue is dropped. The text is checked exactly as sent (HTML isn't stripped and whitespace isn't normalized), so the offsets line up with your own copy of the document. <!-- --> ## Sample Code[​](#sample-code "Direct link to Sample Code") * cURL * JavaScript * Python ``` curl -X POST https://api.sapling.ai/api/v1/inclusive \ -H "Content-Type: application/json" \ -d '{"key":"<api-key>", "text":"The chairman said the plan was crazy, so we need a young, energetic team."}' ``` ``` import axios from 'axios'; async function run(text) { try { const response = await axios.post( 'https://api.sapling.ai/api/v1/inclusive', { key: '<api-key>', text, }, ); 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('The chairman said the plan was crazy, so we need a young, energetic team.'); ``` ``` import requests from pprint import pprint response = requests.post( "https://api.sapling.ai/api/v1/inclusive", json={ "key": "<api-key>", "text": "The chairman said the plan was crazy, so we need a young, energetic team.", } ) if 200 <= response.status_code < 300: pprint(response.json()) else: print('Error: ', response.status_code, response.text) ``` ## Sample Response[​](#sample-response "Direct link to Sample Response") ``` { "issues": [ { "category": "age", "severity": "medium", "text": "young, energetic team", "start": 51, "end": 72, "note": "Frames youth as a requirement, implying older workers are less capable.", "suggestion": "energetic team" }, { "category": "gender", "severity": "low", "text": "The chairman", "start": 0, "end": 12, "note": "Gendered job title used as the default; gender-neutral alternatives are available.", "suggestion": "The chair" }, { "category": "disability", "severity": "low", "text": "crazy", "start": 31, "end": 36, "note": "Uses a mental-health term as a casual pejorative for 'unreasonable'.", "suggestion": "unworkable" } ], "categories": ["gender", "race_ethnicity", "disability", "age", "lgbtq", "religion", "socioeconomic", "appearance"], "inclusive": false } ``` ## Batch Requests[​](#batch-requests "Direct link to Batch Requests") To check many short texts in one request (a queue of job postings or a set of product descriptions, say), send `texts` (a list of 1–10 strings) instead of `text`. The same `categories` apply to every item, and the combined length of all items can be up to 10,000 characters, the same cap as a single `text`. Provide exactly one of `text` and `texts`. ``` curl -X POST https://api.sapling.ai/api/v1/inclusive \ -H "Content-Type: application/json" \ -d '{"key":"<api-key>", "texts": ["Hey guys, the demo is at 3pm.", "Thanks for the quick review."], "categories": ["gender"]}' ``` The batch response is `{"results": [...]}` with one entry per input, in input order. Each entry has the single-response shape shown above, and its offsets index that item's own text: ``` { "results": [ { "issues": [ {"category": "gender", "severity": "low", "text": "guys", "start": 4, "end": 8, "note": "Masculine term used to address a mixed group.", "suggestion": "everyone"} ], "categories": ["gender"], "inclusive": false }, {"issues": [], "categories": ["gender"], "inclusive": true} ] } ``` Each item is billed individually (a batch of N costs the same as N single requests). If any item fails, the whole request returns a `502` and nothing is billed. The items that succeeded are already cached, so a retry only re-runs the failures. ## Request Parameters[​](#request-parameters "Direct link to Request Parameters") **POST** to `https://api.sapling.ai/api/v1/inclusive` **key**: String<br />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<br />The text to check, up to 10,000 characters. It is checked exactly as sent, so the returned offsets line up with your original. Provide exactly one of `text` and `texts`. **texts**: List\[String]<br />Batch form (see [Batch Requests](#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. **categories**: List\[String]<br />Optional list of categories to check. Defaults to all of them. Supported values: | Category | What it covers | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `gender` | Gendered defaults and generics ("chairman", "manpower"), gender stereotypes, unnecessary gender marking ("female engineer") | | `race_ethnicity` | Racial or ethnic stereotypes, outdated or derogatory terms for groups, idioms with racist origins ("blacklist", "grandfathered") | | `disability` | Ableist idioms ("crazy", "lame", "tone-deaf"), defining people by a condition ("wheelchair-bound"), casual use of mental-health terms ("so OCD") | | `age` | Ageist assumptions or framing ("digital native", "young and energetic team") | | `lgbtq` | Heteronormative or cisnormative assumptions and outdated terms ("sexual preference", "transgendered") | | `religion` | Religious stereotypes, assuming a shared faith or holiday | | `socioeconomic` | Classist framing ("ghetto", "trashy") and stigmatizing terms | | `appearance` | Body shaming, weight stigma, irrelevant judgments about appearance | ## Response Parameters[​](#response-parameters "Direct link to Response Parameters") **issues**: List<br />The flagged passages, most severe first. Each item has: * `category`: one of the categories listed above. * `severity`: `high` for slurs or plainly derogatory language, `medium` for stereotypes or exclusionary framing, `low` for dated or gendered defaults that most readers wouldn't notice. * `text`: the flagged passage, exactly as it appears in the input. * `start`, `end`: character offsets into the input text, such that `text[start:end]` is the passage, or `null` in the rare case the passage can't be located. * `note`: a brief, neutral explanation of why the wording may exclude or offend. * `suggestion`: a replacement for the passage that keeps its meaning (an empty string if the passage should simply be removed). At most 20 issues are returned per text. **categories**: List\[String]<br />The categories that were checked. **inclusive**: Boolean<br />`true` when no issues were found. ## Notes[​](#notes "Direct link to Notes") * Offsets count Unicode code points (Python `str` indexing), not UTF-16 code units or bytes. In JavaScript, use `Array.from(text)` if the input may contain characters outside the Basic Multilingual Plane. * Results are cached, so repeating a request with the same text and categories is fast. The order you list `categories` in doesn't matter. * A model or upstream failure returns a `502` and is not billed. Retry rather than changing the request. * Also available as the `sapling_inclusive` tool on the [Sapling MCP server](https://sapling.ai/docs/mcp.md) and as `client.inclusive()` in the [JavaScript client](https://www.npmjs.com/package/@saplingai/sapling-js). * For house-style rules you define yourself (brand voice, terminology, formatting), see the [Style Guide API](https://sapling.ai/docs/api/styleguide.md). For toxicity and other unsafe content, see the [Content Safety API](https://sapling.ai/docs/api/safety.md).   --- # Language Detection API The language detection endpoint identifies the language of a given text and returns a confidence score. This is useful for routing text into language-specific processing pipelines before calling other Sapling APIs. Beyond the top prediction, the endpoint can return the runner-up **candidates** with their confidences, and — for mixed-language text — a per-**segment** breakdown showing which sentences are in which language. Every result also says whether the detection is **reliable** enough to act on, and which writing **script** the text is in. Over 170 languages are supported. [Try it out](https://sapling.ai/utilities/lang-detect?utm_campaign=apidocs) <!-- --> ## Sample Code[​](#sample-code "Direct link to Sample Code") * cURL * JavaScript * Python * Python (Sapling client) ``` curl -X POST https://api.sapling.ai/api/v1/langdetect \ -H "Content-Type: application/json" \ -d '{"key":"<api-key>", "text":"This is an English sentence. Esta es una oración en español.", "segments": true}' ``` ``` import axios from 'axios'; async function run(text) { try { const response = await axios.post( 'https://api.sapling.ai/api/v1/langdetect', { key: '<api-key>', text, segments: true, }, ); const {status, data} = response; console.log({status}); console.log(JSON.stringify(data, null, 4)); } catch (err) { const { msg } = err.response.data; console.log({err: msg}); } } run('This is an English sentence. Esta es una oración en español.'); ``` ``` import requests from pprint import pprint response = requests.post( "https://api.sapling.ai/api/v1/langdetect", json={ "key": "<api-key>", "text": "This is an English sentence. Esta es una oración en español.", "segments": True, } ) 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) result = client.langdetect( 'This is an English sentence. Esta es una oración en español.', segments=True, ) print(result['lang'], result['name'], result['conf']) for segment in result['segments']: print(segment['lang'], segment['text']) ``` ## Sample Response[​](#sample-response "Direct link to Sample Response") ``` { "lang": "en", "name": "English", "conf": 0.63, "reliable": false, "script": "Latn", "candidates": [ {"lang": "en", "name": "English", "conf": 0.63}, {"lang": "es", "name": "Spanish", "conf": 0.35}, {"lang": "pt", "name": "Portuguese", "conf": 0.01} ], "segments": [ { "start": 0, "end": 28, "text": "This is an English sentence.", "lang": "en", "name": "English", "conf": 0.98, "reliable": true, "script": "Latn", "candidates": [ {"lang": "en", "name": "English", "conf": 0.98}, {"lang": "sco", "name": "Scots", "conf": 0.01}, {"lang": "nl", "name": "Dutch", "conf": 0.00} ] }, { "start": 29, "end": 60, "text": "Esta es una oración en español.", "lang": "es", "name": "Spanish", "conf": 0.99, "reliable": true, "script": "Latn", "candidates": [ {"lang": "es", "name": "Spanish", "conf": 0.99}, {"lang": "gl", "name": "Galician", "conf": 0.00}, {"lang": "pt", "name": "Portuguese", "conf": 0.00} ] } ] } ``` Without `segments`, the response contains only `lang`, `name`, `conf`, `reliable`, `script` and `candidates`. Here the whole text is split between two languages, so its confidence is low and `reliable` is `false`, while each segment on its own is reliable. ## Batch Requests[​](#batch-requests "Direct link to Batch Requests") To detect the language of many short texts — user messages to route, form submissions — in one request, send `texts` (a list of 1–10 strings) instead of `text`. The same `top_k` and `segments` options apply to every item, and the combined length of all items may be up to 20,000 characters (the same cap as a single `text`, measured after HTML stripping). Exactly one of `text` and `texts` must be provided. ``` curl -X POST https://api.sapling.ai/api/v1/langdetect \ -H "Content-Type: application/json" \ -d '{"key":"<api-key>", "texts": ["This is an English sentence.", "Esta es una oración en español."]}' ``` The batch response is `{"results": [...]}` with one entry per input, in input order; each entry has the single-response shape above (without `segments` by default): ``` { "results": [ { "lang": "en", "name": "English", "conf": 0.98, "reliable": true, "script": "Latn", "candidates": [ {"lang": "en", "name": "English", "conf": 0.98}, {"lang": "sco", "name": "Scots", "conf": 0.01}, {"lang": "nl", "name": "Dutch", "conf": 0.00} ] }, { "lang": "es", "name": "Spanish", "conf": 0.99, "reliable": true, "script": "Latn", "candidates": [ {"lang": "es", "name": "Spanish", "conf": 0.99}, {"lang": "gl", "name": "Galician", "conf": 0.00}, {"lang": "pt", "name": "Portuguese", "conf": 0.00} ] } ] } ``` With `segments: true`, each entry's segment offsets index that entry's own tag-stripped text (the same convention as a single request; see `segments` under [Response Parameters](#response-parameters)). If any item fails to detect, the whole request returns a `400`; a retry re-runs the batch (detection is fast, deterministic compute). ## Request Parameters[​](#request-parameters "Direct link to Request Parameters") **POST** to `https://api.sapling.ai/api/v1/langdetect` **key**: String<br />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<br />Text to identify the language for. Up to 20,000 characters (HTML tags are stripped before detection). Provide exactly one of `text` and `texts`. **texts**: List\[String]<br />Batch form (see [Batch Requests](#batch-requests)): 1–10 texts to detect in one request, each treated exactly like `text` above, with a combined length of up to 20,000 characters. An item that is empty after HTML stripping returns a `400` naming its index. The response becomes `{"results": [...]}`, one entry per item in input order. **top\_k**: Integer<br />Number of language candidates to return, from 1 to 10. Defaults to `3`. Applies to the whole text and to each segment. **segments**: Boolean<br />Whether to also detect the language of each segment of the text. Defaults to `false`. The text is split on line breaks and sentence boundaries; very short fragments are merged with a neighboring sentence on the same line so that tiny snippets don't produce noisy guesses. Use this to find the language of each part of a mixed-language document, or to check whether a document is monolingual. ## Response Parameters[​](#response-parameters "Direct link to Response Parameters") **lang**: String<br />The detected language code for the whole text. Mostly two-letter ISO 639-1 codes (`en`, `es`, `zh`, ...); languages without a two-letter code use their ISO 639-2/3 code (`ceb`, `war`, ...). **name**: String<br />The English name of the detected language (`"English"`, `"Spanish"`, ...). **conf**: Float<br />Confidence score for the detected language, between 0 and 1. For a mixed-language text the whole-text confidence is typically split across the languages present — look at `segments` for the per-part picture. **reliable**: Boolean<br />Whether the detection is confident enough to act on. Use this instead of picking your own `conf` threshold. It is `true` only when the confidence is high **and** the text has enough letters, because the detector can be confidently wrong on very short input: `"OK"` or `"Hola"` is never reliable, however high its `conf`. Currently that means a `conf` above 0.9 and at least 10 letters, with each Chinese, Japanese or Korean character counting as three. A mixed-language text is often unreliable as a whole while each of its `segments` is reliable. **script**: String or null<br />The dominant writing system of the text, as an [ISO 15924](https://en.wikipedia.org/wiki/ISO_15924) code: the script with the most letters wins. Examples are `Latn` (Latin), `Cyrl` (Cyrillic), `Grek` (Greek), `Arab` (Arabic), `Hebr` (Hebrew), `Deva` (Devanagari), `Thai` (Thai) and `Hang` (Korean Hangul). Chinese characters alone give `Hani`; once the text contains any Japanese kana, its Chinese characters and kana count together as `Jpan`. Fullwidth and halfwidth forms count as their base script. Letters from a script outside this list give `Zzzz`, and a text with no letters at all (digits, punctuation or emoji only) gives `null`. `script` is computed from the characters themselves, independently of `lang`, so it can tell Serbian in Cyrillic from Serbian in Latin. **candidates**: List<br />The top `top_k` languages for the whole text, most likely first. Each item has `lang`, `name` and `conf`. The first candidate is the same as the top-level `lang`/`conf`. **segments**: List<br />Only present when `segments` is `true`. One item per detected segment, in document order, each with: * `start`, `end`: character offsets of the segment into the (tag-stripped) input text, such that `text[start:end]` is the segment. * `text`: the segment text. * `lang`, `name`, `conf`, `reliable`, `script`, `candidates`: the detection result for that segment, in the same shape as the top level.   --- # Named-Entity Recognition API The NER endpoint finds named entities in a text — people, organizations, locations, dates, times, money amounts, percentages, quantities, products and events — and returns each one with exact character offsets into the text you sent. Recognition is model-based, but every entity is grounded: each mention the model proposes is located in your text before it is returned, and a mention the text does not actually contain is dropped. The text is never tag-stripped or normalized on this endpoint, so the returned offsets line up with your own copy of the document — the same offsets contract as the [PII API](https://sapling.ai/docs/api/pii.md). The two endpoints are complements: PII detection finds format- and checksum-validated identifiers (emails, card numbers, IBANs), NER finds names and real-world references. For one-call PII **redaction** that also covers people, organizations and locations, the PII endpoint can run this recognizer for those three types — [opt in](https://sapling.ai/docs/api/pii.md#named-entity-pii-opt-in) by listing them in its `types` parameter. <!-- --> ## Sample Code[​](#sample-code "Direct link to Sample Code") * cURL * JavaScript * Python ``` curl -X POST https://api.sapling.ai/api/v1/ner \ -H "Content-Type: application/json" \ -d '{"key":"<api-key>", "text":"Sapling opened a Toronto office on March 3, 2026."}' ``` ``` import axios from 'axios'; async function run(text) { try { const response = await axios.post( 'https://api.sapling.ai/api/v1/ner', { key: '<api-key>', text, }, ); 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('Sapling opened a Toronto office on March 3, 2026.'); ``` ``` import requests from pprint import pprint response = requests.post( "https://api.sapling.ai/api/v1/ner", json={ "key": "<api-key>", "text": "Sapling opened a Toronto office on March 3, 2026.", } ) if 200 <= response.status_code < 300: pprint(response.json()) else: print('Error: ', response.status_code, response.text) ``` ## Sample Response[​](#sample-response "Direct link to Sample Response") ``` { "entities": [ { "text": "Sapling", "type": "organization", "start": 0, "end": 7 }, { "text": "Toronto", "type": "location", "start": 17, "end": 24 }, { "text": "March 3, 2026", "type": "date", "start": 35, "end": 48 } ], "types": ["date", "location", "organization"] } ``` ## Batch Requests[​](#batch-requests "Direct link to Batch Requests") To analyze many short texts — a queue of support tickets, a set of retrieved documents — in one request, send `texts` (a list of 1–10 strings) instead of `text`. The same `types` 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/ner \ -H "Content-Type: application/json" \ -d '{"key":"<api-key>", "texts": ["Sapling is hiring in Toronto.", "Invoices over $500 need approval by Friday."]}' ``` 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 offsets index that item's own text: ``` { "results": [ { "entities": [ {"text": "Sapling", "type": "organization", "start": 0, "end": 7}, {"text": "Toronto", "type": "location", "start": 21, "end": 28} ], "types": ["location", "organization"] }, { "entities": [ {"text": "$500", "type": "money", "start": 14, "end": 18}, {"text": "Friday", "type": "date", "start": 36, "end": 42} ], "types": ["date", "money"] } ] } ``` 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. ## Request Parameters[​](#request-parameters "Direct link to Request Parameters") **POST** to `https://api.sapling.ai/api/v1/ner` **key**: String<br />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<br />The text to analyze, up to 10,000 characters. The text is analyzed exactly as sent — HTML is **not** stripped and whitespace is **not** normalized — so that the returned offsets line up with your original. Pass the form of the document you want spans into; for the best recognition quality, prefer plain text over heavy markup. Provide exactly one of `text` and `texts`. **texts**: List\[String]<br />Batch form (see [Batch Requests](#batch-requests)): 1–10 texts to analyze 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. **types**: List<br />Optional list of entity types to recognize. Defaults to all types. Supported values: | Type | What it matches | | -------------- | ---------------------------------------------------------------------------------------------- | | `person` | Named people, real or fictional, including titles that are part of the name ("Dr. Jane Smith") | | `organization` | Companies, agencies, institutions, teams and other named groups | | `location` | Countries, cities, regions, street addresses, landmarks and geographic features | | `date` | Calendar expressions, absolute or relative ("March 5, 2026", "last Tuesday", "Q3 2025") | | `time` | Times of day and clock expressions ("3pm", "14:30", "noon") | | `money` | Monetary amounts including their currency symbol or unit ("$1,299.00", "20 euros") | | `percent` | Percentages, including the sign or the word ("25%", "twenty percent") | | `quantity` | Measurements with their unit ("5 km", "two litres", "3.5 GHz") | | `product` | Named products, models, vehicles and software ("iPhone 15", "Boeing 747") | | `event` | Named events: conferences, sports events, holidays, wars ("WWDC 2026", "the Olympics") | ## Response Parameters[​](#response-parameters "Direct link to Response Parameters") **entities**: List<br />The recognized entities, in document order. Each item has: * `text`: the entity's text, exactly as it appears in the input. * `type`: one of the types listed above. * `start`, `end`: character offsets into the input text, such that `text[start:end]` is the entity. Every occurrence of a recognized mention is returned as its own span, so an entity that appears three times in the document produces three items. **types**: List<br />The sorted list of distinct entity types found in the text. ## Notes[​](#notes "Direct link to Notes") * Offsets count Unicode code points (Python `str` indexing), not UTF-16 code units or bytes. In JavaScript, use `Array.from(text)` if the input may contain characters outside the Basic Multilingual Plane. * Results are cached, so repeating a request (or retrying the failed items of a batch) with the same text and types is fast and is not billed twice. * A model or upstream failure returns a `502` and is not billed — retry rather than changing the request. * For format- and checksum-validated identifiers — emails, phone numbers, card numbers, IBANs — with the same offsets contract, see the [PII API](https://sapling.ai/docs/api/pii.md). That endpoint can also [opt in](https://sapling.ai/docs/api/pii.md#named-entity-pii-opt-in) to `person`/`organization`/`location` recognition, redacting identifiers and names in one call; the recognition is shared with this endpoint (same cache and billing identity for the same text and types).   --- # 5 Best Paraphrase APIs [Try the Sapling API](https://sapling.ai/docs/api/rephrase.md)       [Try Sapling's Sentence Rewriter](https://sapling.ai/utilities/sentence-rewriter) ## Paraphrasing[​](#paraphrasing "Direct link to Paraphrasing") **Paraphrasing**, **rephrasing**, **sentence rewriting**, or **word spinning** are all terms referring to the same task of taking some input text and providing an alternative phrasing of that text. For example, given the sentence, `Hi, how are you?`, a rephrasing system might suggest the following paraphrases: ``` 1. Hello! I hope you're doing well. 2. Hi there -- how is your day going? 3. Greetings, how are you feeling today? 4. Hey! I'd be happy to know how you're doing. ``` The specific number of paraphrases will depend on the system and the user's request -- many systems can be run repeatedly to generate new paraphrases through a technique called **sampling**, which we'll describe briefly below. Beyond simply generating an arbitrary number of different ways to phrase a certain sentence, it's easy to imagine additional capabilities for a paraphrasing system: * **Tone**: The user may desire that the rephrased sentence have some target tone, e.g. *paraphrase this sentence so that it sounds friendlier*. Sapling offers both a [tone detection system](https://sapling.ai/utilities/tone?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs) as well as a system for [rephrasing while specifying a target tone](https://sapling.ai/sentence-rewriter?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs). * **Formality**: Make this sound more formal / professional. This is a common use case for business communication. * **Length / readability**: Make this piece of text shorter or simpler. Some summarization systems -- loosely speaking -- fall under the umbrella of paraphrasing systems. Taking a step back, the goal of a paraphrasing system is simply to provide the user with ideas of how to best express a given statement. But how do we measure what is *best* in this case? ## Evaluating paraphrase systems[​](#evaluating-paraphrase-systems "Direct link to Evaluating paraphrase systems") When evaluating a paraphrasing system, there are two common criteria: 1. **Meaning preservation**: Obviously if the meaning of the input text is significantly changed, the paraphrase is no good. 2. **Diversity**: On the other hand, if the paraphrasing system simply spits out the same sentence as the input sentence, the system is also useless (even though meaning is perfectly preserved). This is visualized in the chart below: ![Example trade-off curve between meaning preservation and diversity](/img/diversity_meaning_tradeoff.svg) Example trade-off curve between meaning preservation and diversity. There are other criteria that are not specific to paraphrasing systems -- latency / generation time, as just one example -- but the two above are a good starting point. Going back to sampling: sometimes paraphrasing systems use language models where the user can adjust the **temperature**. This parameter is effectively a way to trade off meaning preservation and diversity, where lower temperatures result in more deterministic, similar outputs, while higher temperatures result in more diverse outputs. Without jumping into the technical details, unless you've gathered a decent evaluation dataset to adjust this knob, we recommend consulting with the authors of the paraphrasing system for their recommendations. ## Application programming interfaces (APIs)[​](#application-programming-interfaces-apis "Direct link to Application programming interfaces (APIs)") For those unfamiliar with APIs, we recommend reading the overview in our other [comparison](https://sapling.ai/docs/api/comparison.md#application-programming-interfaces). In brief, the API exposes a set of web endpoints that take input texts / queries along with other criteria (such as the target tone, for example) and returns back a list of rephrases that (hopefully) match the criteria. ## Software development kits (SDKs)[​](#software-development-kits-sdks "Direct link to Software development kits (SDKs)") Currently, Sapling is unaware of a provider that offers a paraphrase SDK offering the UI components for displaying rephrase suggestions. [Contact us](https://sapling.ai/contact?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs) if you'd like access to our system in development. ## Building your own[​](#building-your-own "Direct link to Building your own") Before we consider some of the providers of paraphrase systems, a common question is: why not just build your own? With the rise of LLMs, it's possible to create prompts for LLMs to generate rephrases of sentences in a given style. While this is feasible, here are a few considerations for why you may wish to consider a pre-built, task-specific solution instead: 1. **Predictability**: Depending on the LLM system, it may not generate the output in an always consistent, structured format where it is easy to extract the suggestions. 2. **Cost**: Depending on the system, it may be cheaper to use a smaller, specialized model for the task. 3. **Prompt injection and backdoors**: Because LLMs are so general, they are capable of performing rephrasing as well as other tasks when given the right prompt. This can pose security concerns for organizations, especially when the LLM may be capable of generating inappropriate content or has been trained on sensitive data, as [prompt injection](https://simonwillison.net/2023/Apr/14/worst-that-can-happen/) can result in undesired behavior. 4. **Data security and self-hosting**: Cutting edge LLMs are generally offered through web-based APIs that require sending texts to remote servers. Depending on the application and data security requirements, it may not be possible to use a cloud service. 5. **Maintenance**: Finally, while the backend maintenance overhead may not be significant when using a third-party LLM endpoint, the maintenance of the frontend system may require engineering time that is better spent elsewhere. ## Providers[​](#providers "Direct link to Providers") As of this writing, here are some common paraphrasing tools that offer API access: * [Sapling.ai](https://sapling.ai/docs/api/rephrase?utm_campaign=apidocs): Sapling not only offers a paraphrasing API, but is also a fully-featured AI writing assistant with grammar and spell checking, autocomplete, suggested snippets, and more. Its paraphrase API offers options such as a target tone or formality. ![](https://sapling.ai/static/img/sapling-logo-horizontal.svg) * [AI21](https://docs.ai21.com/docs/paraphrase-api): AI21 offers the popular Wordtune application, available as a browser extension or as a website application. As part of the AI21 Studio that offers task-specific APIs, AI21 offers a paraphrasing API that also powers its Wordtune service. Styles include `general`, `casual`, `formal`, `long`, and `short`. ![AI21](/img/ai21-logo.svg) * [Neural Writer](https://neuralwriter.com/paraphrasing-api/): Neural Writer offers a paraphrasing API through the RapidAPI platform. It claims to support 27 different languages. A corresponding browser extension is available along with a demo on its homepage. The paraphraser offers `light`, `medium`, and `hard` modes to adjust the amount of rephrasing performed. ![Neural Writer](/img/neural-writer-logo.svg) * [Paraphrasing Tool](https://www.paraphraser.io/paraphrasing-api-documentation): Paraphrasing Tool offers a web interface and API with support for six different modes: `word changer`, `fluency`, `advanced`, `creative`, `smarter`, and `shorten`. It claims to support eight different languages. ![Paraphrasing Tool](/img/paraphrasing-tool-logo.png) * Formerly part of, but now notably absent from the list is QuillBot. Perhaps the company that popularized paraphrasing models, QuillBot offers paraphrasing, grammar checking, and other tools such as citation generation and plagiarism checking for students. It states very clearly in its [help documentation](https://help.quillbot.com/hc/en-us/articles/4541472549527-Does-QuillBot-Offer-an-API), however, that it does not offer a paraphrasing API. ## Comparison[​](#comparison "Direct link to Comparison") | Provider | Main Benefits | Self-Hosting? | Other Features | | ----------------- | ---------------------------------------------------------------------------------------------- | ------------- | ---------------------------------------------------- | | Sapling.ai | Advanced paraphrase model with upcoming SDK. Other easily integrated AI writing functionality. | Yes | Target tones for the rephrase model, on-prem option. | | AI21 | Established task-specific model used to power Wordtune. LLM offering as well. | No | Python and JavaScript client libraries. | | Neural Writer | Paraphrasing model with adjustability of level of rephrasing desired. | No | Support for 27 languages. | | Paraphrasing Tool | Similar to QuillBot in offering different paraphrase modes. | No | Support for 8 languages. | | QuillBot | NA | NA | NA | ## Conclusion[​](#conclusion "Direct link to Conclusion") The landscape of tools that language models are enabling is rapidly evolving. Despite this, at present there are a small set of APIs that offer turnkey paraphrasing functionality. These make it straightforward to add AI-powered rephrase suggestions to your application. We review four (previously five) such options above, but obviously, we're biased: [Try the Sapling API](https://sapling.ai/docs/api/rephrase.md) --- # PII Detection & Redaction API The PII endpoint finds personally identifiable information in a text — email addresses, phone numbers, US Social Security numbers, credit card numbers, IP addresses, IBANs and US bank routing numbers — and optionally returns a redacted copy of the text with each entity replaced by a placeholder such as `[EMAIL]`. Detection of these types is deterministic: every candidate must pass a structural check (for example a Luhn checksum for card numbers, a mod-97 checksum for IBANs, and the ABA checksum for routing numbers), so precision stays high without a machine-learning model, and results are fully reproducible. Every entity comes with exact character offsets into the text you sent, so you can apply the redactions to your own copy of a document — a support transcript, a chat log, an LLM prompt — before storing it or sending it on. Person names, organizations and locations / street addresses need a named-entity model rather than a checksum, so they are **not** detected by default. To find and redact them in the same call, list `person`, `organization` or `location` in `types` explicitly — see [Named-entity PII (opt-in)](#named-entity-pii-opt-in) below. A request that does not list them is byte-for-byte the deterministic endpoint described above: no model call, no added latency, no added billing. For named entities beyond those three types (dates, money amounts, products…) without redaction, use the [NER API](https://sapling.ai/docs/api/ner.md), which shares the same offsets contract. [Try it out](https://sapling.ai/utilities/pii-redaction?utm_campaign=apidocs) <!-- --> ## Sample Code[​](#sample-code "Direct link to Sample Code") * cURL * JavaScript * Python * Python (Sapling client) ``` curl -X POST https://api.sapling.ai/api/v1/pii \ -H "Content-Type: application/json" \ -d '{"key":"<api-key>", "text":"Reach me at jane.doe@example.com or (415) 555-0132.", "redact": true}' ``` ``` import axios from 'axios'; async function run(text) { try { const response = await axios.post( 'https://api.sapling.ai/api/v1/pii', { key: '<api-key>', text, redact: true, }, ); const {status, data} = response; console.log({status}); console.log(JSON.stringify(data, null, 4)); } catch (err) { const { msg } = err.response.data; console.log({err: msg}); } } run('Reach me at jane.doe@example.com or (415) 555-0132.'); ``` ``` import requests from pprint import pprint response = requests.post( "https://api.sapling.ai/api/v1/pii", json={ "key": "<api-key>", "text": "Reach me at jane.doe@example.com or (415) 555-0132.", "redact": True, } ) 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) result = client.pii( 'Reach me at jane.doe@example.com or (415) 555-0132.', redact=True, ) for entity in result['entities']: print(entity['type'], entity['start'], entity['end'], entity['text']) print(result['redacted_text']) ``` ## Sample Response[​](#sample-response "Direct link to Sample Response") ``` { "entities": [ { "type": "email", "text": "jane.doe@example.com", "start": 12, "end": 32, "replacement": "[EMAIL]" }, { "type": "phone", "text": "(415) 555-0132", "start": 36, "end": 50, "replacement": "[PHONE]" } ], "flagged": true, "types": ["email", "phone"], "redacted_text": "Reach me at [EMAIL] or [PHONE]." } ``` Without `redact`, the response contains only `entities`, `flagged` and `types`. ## Named-entity PII (opt-in)[​](#named-entity-pii-opt-in "Direct link to Named-entity PII (opt-in)") Listing `person`, `organization` or `location` in `types` adds LLM-backed named-entity detection to the same request, so one call redacts both the checksum-validated identifiers and the names around them: ``` curl -X POST https://api.sapling.ai/api/v1/pii \ -H "Content-Type: application/json" \ -d '{"key":"<api-key>", "text":"Ask Jane Smith (jane@acme.com) about Acme Corp in Paris.", "types": ["email", "person", "organization", "location"], "redact": true}' ``` ``` { "entities": [ {"type": "person", "text": "Jane Smith", "start": 4, "end": 14, "replacement": "[PERSON]"}, {"type": "email", "text": "jane@acme.com", "start": 16, "end": 29, "replacement": "[EMAIL]"}, {"type": "organization", "text": "Acme Corp", "start": 37, "end": 46, "replacement": "[ORGANIZATION]"}, {"type": "location", "text": "Paris", "start": 50, "end": 55, "replacement": "[LOCATION]"} ], "flagged": true, "types": ["email", "location", "organization", "person"], "redacted_text": "Ask [PERSON] ([EMAIL]) about [ORGANIZATION] in [LOCATION]." } ``` The named-entity types are strictly opt-in and behave differently from the deterministic ones: * **Only when listed.** They never run by default; a request without an explicit `types` list, or with a list that omits them, stays fully deterministic. * **Length cap.** A request that lists any of them is capped at **10,000** characters (the [NER API](https://sapling.ai/docs/api/ner.md)'s cap) rather than 20,000, because the whole text is analyzed by the model in one pass. * **Billing.** On top of the per-character PII rate, the request bills one NER-priced unit for the submitted characters — see [Pricing](https://sapling.ai/docs/api/pricing). The recognition shares its cache and its 3-day billing identity with `/api/v1/ner`, so submitting the same text and named-entity types through either endpoint is billed as one unit of recognition work, not two. * **Failures are loud.** If the model call fails, the whole request returns a `502` and nothing is billed or cached — a partial response missing the requested name spans would read as "no such PII here", which is exactly the silent leak a redaction pipeline cannot tolerate. Retry rather than changing the request. * **Trial rate limit.** On trial keys, cache-missing model calls are rate-limited; when exhausted the request returns a `429` with a `Retry-After` header (in seconds). The allowance is shared with `/api/v1/ner`. * **Overlaps.** If a model-reported span overlaps a deterministic detection, the deterministic span wins — it is checksum-validated against the exact characters it covers (the model may, for example, report an email address as an organization mention), and the higher-precision placeholder is kept. ## Request Parameters[​](#request-parameters "Direct link to Request Parameters") **POST** to `https://api.sapling.ai/api/v1/pii` **key**: String<br />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<br />Text to scan for PII. Up to 20,000 characters, or 10,000 when `types` includes a [named-entity type](#named-entity-pii-opt-in). The text is scanned exactly as sent — HTML is **not** stripped and whitespace is **not** normalized — so that the returned offsets line up with your original. Pass plain text rather than HTML markup for the best results. Text containing a NUL character (`U+0000`) is rejected with a `400` (it cannot be stripped without shifting every later offset, and never occurs in legitimate text). **types**: List<br />Optional list of PII types to detect. Defaults to all types. Supported values: | Type | What it matches | Validation | | ----------------- | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | | `email` | Email addresses | Address format | | `phone` | Phone numbers, national or international (`+`) format, with common separators | Digit count and separator shape; dates and SSN-shaped numbers are rejected | | `ssn` | US Social Security numbers | 3-2-4 groups with a consistent separator, plus SSA area/group/serial rules | | `credit_card` | Payment card numbers (Visa, Mastercard, American Express, Discover, JCB, Diners Club, UnionPay, Maestro) | Luhn checksum and known issuer prefixes | | `ip_address` | IPv4 and IPv6 addresses | Parsed as an IP address; version strings and times are rejected | | `iban` | International Bank Account Numbers | Mod-97 checksum and per-country length | | `us_bank_routing` | US ABA bank routing numbers | ABA checksum and Federal Reserve prefix ranges | | `person` | Person names — [opt-in](#named-entity-pii-opt-in), never run by default | LLM-backed named-entity recognition | | `organization` | Companies, agencies, institutions — [opt-in](#named-entity-pii-opt-in) | LLM-backed named-entity recognition | | `location` | Locations and street addresses — [opt-in](#named-entity-pii-opt-in) | LLM-backed named-entity recognition | The last three run **only when listed explicitly**; "defaults to all types" means all deterministic types. **redact**: Boolean<br />If `true`, the response also includes `redacted_text`: the input with every detected entity replaced by its `replacement` placeholder. Defaults to `false`. ## Response Parameters[​](#response-parameters "Direct link to Response Parameters") **entities**: List<br />The detected PII entities, in document order. Spans never overlap; where two candidate matches overlap, the checksum-validated type wins (a Luhn-valid card number is never also reported as a phone number). Each item has: * `type`: one of the types listed above. * `text`: the matched text, exactly as it appears in the input. * `start`, `end`: character offsets into the input text, such that `text[start:end]` is the entity. * `replacement`: the placeholder used for this type in `redacted_text`, e.g. `[EMAIL]`, `[PHONE]`, `[SSN]`, `[CREDIT_CARD]`, `[IP_ADDRESS]`, `[IBAN]`, `[US_BANK_ROUTING]`, `[PERSON]`, `[ORGANIZATION]`, `[LOCATION]`. **flagged**: Boolean<br />`true` if at least one entity was detected. **types**: List<br />The sorted list of distinct PII types found in the text. **redacted\_text**: String<br />Only present when `redact` is `true`. The input text with each entity replaced by its placeholder. Because the offsets refer to the original text, you can also build your own redaction (for example masking all but the last four digits of a card number) by splicing `entities` into your copy of the text. ## Notes[​](#notes "Direct link to Notes") * Offsets count Unicode code points (Python `str` indexing), not UTF-16 code units or bytes. In JavaScript, use `Array.from(text)` if the input may contain characters outside the Basic Multilingual Plane. * Because detection of the deterministic types is pattern-based, a syntactically valid but fictitious value (a test card number, a made-up email address) is still reported; the endpoint answers "does this look like PII", not "does this identify a real person". * For guardrail patterns around LLM inputs and outputs — including secrets and profanity — see [Guardrails](https://sapling.ai/docs/api/guardrails.md); for token-level profanity flagging see the [Profanity API](https://sapling.ai/docs/api/profanity.md).   --- # Postprocessing The Postprocess endpoint performs a variety of operations that are useful for working with the outputs of an NLP (whether human or AI) system. These include: * Fixing or restoring punctuation * Fixing capitalization * Fixing or restoring whitespace * Splitting sentences into shorter sentences Example use cases include repairing transcriptions or captions. info If you're looking for a more complete solution that also checks for spelling and grammar errors, consider using the [Edits](https://sapling.ai/docs/api/edits-overview.md) endpoint instead. Looking for something not provided here? [Contact us](https://sapling.ai/contact?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs). ## Postprocess POST[​](#postprocess-post "Direct link to Postprocess POST") ### Request Parameters[​](#request-parameters "Direct link to Request Parameters") `https://api.sapling.ai/api/v1/postprocess` HTTP method: `POST` The Postprocess API POST endpoint takes JSON parameters documented below: **key**: String<br />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<br />Text to postprocess **session\_id**: Optional String<br />Unique name or UUID of document or portion of text that is being checked. If not provided, defaults to current date in `YYYYMMDD` format. **operations**: String List<br />Operations to apply. The currently accepted operations are: * `capitalize` * `punctuate` * `fixspaces` * `split` (Beta: please contact us if you encounter any issues) **auto\_apply**: Optional Boolean<br />Default is false. If true, result with have extra field `applied_text` containing text with edits applied. Example JSON body: ``` { "key": "<api-key>", "text": "Hey how are you doing", "session_id": "test_session", "operations": ["capitalize", "punctuate"] } ``` ### Response Parameters[​](#response-parameters "Direct link to Response Parameters") The Postprocess endpoint returns JSON following the same structure as the [edits endpoint](https://sapling.ai/docs/api/edits-overview.md). **edits**: Array<br />Array of `Edit` objects for the text sent in the request. Example output: ``` { "edits": [ { "end": 3, "error_type": "M:PUNCT", "general_error_type": "Punctuation", "id": "b8d79060-d244-59ef-8dc8-9fdd02216454", "replacement": "Hey,", "sentence": "Hey how are you doing", "sentence_start": 0, "start": 0 }, { "end": 21, "error_type": "M:PUNCT", "general_error_type": "Punctuation", "id": "5b5f7598-91ef-55db-9359-0c88e11e5259", "replacement": "doing?", "sentence": "Hey how are you doing", "sentence_start": 0, "start": 16 } ] } ```   --- # Preprocessing Overview ## Ingestion + Extraction[​](#ingestion--extraction "Direct link to Ingestion + Extraction") By extraction, we mean the process by which you turn structured documents (such as PDFs, Word documents, and webpages) into plain or structured text that you can send to the Sapling API. Sapling currently supports rich HTML for some of its endpoints (including the [Edits](https://sapling.ai/docs/api/edits-overview.md) endpoint), and also offers extractors for [PDF and DOCX files](https://sapling.ai/docs/api/files.md). We may add extractors for other input types in the future. Contact us if you need help with this. ## Chunking / Segmentation[​](#chunking--segmentation "Direct link to Chunking / Segmentation") Please see our separate [Chunking](https://sapling.ai/docs/api/chunk.md) page. ## Tokenization[​](#tokenization "Direct link to Tokenization") Tokenization is the splitting of a piece of text into pieces, called *tokens*. This is usually done at a smaller level than chunking. Tokens can be characters, words, or even *wordpieces* (subwords). For more background, please refer to this [Stanford NLP article](https://nlp.stanford.edu/IR-book/html/htmledition/tokenization-1.html). Tokenization is usually part of an NLP system's preprocessing pipeline. The purpose of tokenization is to split the text into pieces that are easier to work with. For example, suppose you want to find sentences in a document that exceed a certain length. This would first require you to perform *sentence tokenization* (or *sentence segmentation*). As another example, suppose you want to build a word cloud visualization. You would first need to split the text into words before counting the frequency of each word and then creating the visualization. Tokenization is usually not as simple as splitting on whitespace (in English, are contractions one or two words?). There are many tokenization schemes to split text into sentences and words. As hinted at above, there are even methods to segment words into subwords based on information theoretic principles--this is especially useful for agglutinative languages. Sapling currently does not offer APIs for tokenization. However, we recommend trying [NLTK](https://www.nltk.org/) and [Stanza](https://stanfordnlp.github.io/stanza/) for your tokenization needs. If you're an existing customer, reach out if you need help. --- # Profanity Filter API The profanity API checks for profanity in the given text - it provides a 0/1 label for each token, where 1 indicates profanity was detected. Note that as with any profanity check API, there will be terms specific to different industries and audiences that are not captured; please [contact us](https://sapling.ai/contact?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs) with any feedback. [Try it out](https://sapling.ai/utilities/profanity?utm_campaign=apidocs) <!-- --> ## Sample Code[​](#sample-code "Direct link to Sample Code") * cURL * JavaScript * JavaScript SDK * Python ``` curl -X POST https://api.sapling.ai/api/v1/profanity \ -H "Content-Type: application/json" \ -d '{"key":"<api-key>", "text":"This is so damn stupid"}' ``` ``` import axios from 'axios'; async function run(text) { try { const response = await axios.post( 'https://api.sapling.ai/api/v1/profanity', { key: '<api-key>', text, }, ); const {status, data} = response; console.log({status}); console.log(JSON.stringify(data, null, 4)); } catch (err) { const { msg } = err.response.data; console.log({err: msg}); } } run("This is so damn stupid."); ``` ``` // npm install @saplingai/sapling-js import { Client } from '@saplingai/sapling-js/client'; const client = new Client('<api-key>'); const { data } = await client.profanity('This is so damn stupid.'); const flagged = data.toks.filter((tok, i) => data.labels[i] === 1); console.log(flagged); // [ 'damn', 'stupid.' ] ``` ``` import requests from pprint import pprint response = requests.post( "https://api.sapling.ai/api/v1/profanity", json={ "key": "<api-key>", "text": "This is so damn stupid." } ) if 200 <= response.status_code < 300: pprint(response.json()) else: print('Error: ', response.status_code, response.text) ``` ## Sample Response[​](#sample-response "Direct link to Sample Response") ``` { "labels": [ 0, 0, 0, 1, 1 ], "toks": [ "This", "is", "so", "damn", "stupid." ] } ``` ## Request Parameters[​](#request-parameters "Direct link to Request Parameters") **POST** to `https://api.sapling.ai/api/v1/profanity` **key**: String<br />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<br />Text to check for profanity. ## Response Parameters[​](#response-parameters "Direct link to Response Parameters") The profanity filter API response contains two fields: 1. `toks`: The tokens (words) detected in the sentence. The text is split on whitespace, so punctuation stays attached to the token (e.g. `stupid.`). 2. `labels`: For each token, 0 (not profanity) or 1 (profanity).   --- # Quality Score The quality endpoint computes a writing-quality score for a given text. The base **score** (1–5) is based on how "surprising" the text is to a language model trained on well-formed text. If a text is highly surprising, that often indicates it contains errors or is a low-quality text, so high surprisal corresponds to a lower score; text that is not surprising to the model gets a higher score. This score mostly measures syntax and grammaticality and is *not* intended to evaluate factors such as the originality of a text. Because it is model-based, fine-tuning can often greatly improve performance for a specific domain. Two optional measurements make the score actionable: * **`sentence_scores`** — the same 1–5 score for each sentence, with character offsets, so you can find the weakest sentences in a document. Computed from the same model pass as the base score. * **`rubric`** — an LLM-judged rubric: an overall score, scores on four dimensions (**clarity**, **coherence**, **correctness**, **concision**), a short summary, and a list of concrete **issues**, each quoting the problematic excerpt with its offsets, a note, and a suggested rewrite. The rubric is billed at the Tone rate (see [Pricing](https://sapling.ai/docs/api/pricing)); the base score and `sentence_scores` are not. [Try it out](https://sapling.ai/utilities/quality?utm_campaign=apidocs) <!-- --> ## Sample Code[​](#sample-code "Direct link to Sample Code") * cURL * JavaScript * Python * Python (Sapling client) ``` curl -X POST https://api.sapling.ai/api/v1/quality \ -H "Content-Type: application/json" \ -d '{"key":"<api-key>", "text":"Thanks for reaching out about your order. In order to process the refund we will need for you to provide us with the order number. Once we recieve it, the refund will be issued within 3-5 business days.", "sentence_scores": true, "rubric": true}' ``` ``` import axios from 'axios'; async function run(text) { try { const response = await axios.post( 'https://api.sapling.ai/api/v1/quality', { key: '<api-key>', text, sentence_scores: true, rubric: true, }, ); 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('Thanks for reaching out about your order. In order to process the refund we will need for you to provide us with the order number. Once we recieve it, the refund will be issued within 3-5 business days.'); ``` ``` import requests from pprint import pprint response = requests.post( "https://api.sapling.ai/api/v1/quality", json={ "key": "<api-key>", "text": "Thanks for reaching out about your order. In order to process the refund we will need for you to provide us with the order number. Once we recieve it, the refund will be issued within 3-5 business days.", "sentence_scores": True, "rubric": True, } ) 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) result = client.quality( 'Thanks for reaching out about your order. In order to process the refund we will need for you to provide us with the order number. Once we recieve it, the refund will be issued within 3-5 business days.', sentence_scores=True, rubric=True, ) print(result['score'], result['rubric']['overall'], result['rubric']['dimensions']) weakest = min(result['sentences'], key=lambda s: s['score']) print('Weakest sentence:', weakest['text'], weakest['score']) for issue in result['rubric']['issues']: print(issue['dimension'], '|', issue['quote'], '->', issue['suggestion']) ``` ## Sample Response[​](#sample-response "Direct link to Sample Response") ``` { "score": 3.41, "sentences": [ {"start": 0, "end": 41, "text": "Thanks for reaching out about your order.", "score": 4.62}, {"start": 42, "end": 130, "text": "In order to process the refund we will need for you to provide us with the order number.", "score": 3.05}, {"start": 131, "end": 202, "text": "Once we recieve it, the refund will be issued within 3-5 business days.", "score": 2.71} ], "rubric": { "overall": 3, "dimensions": { "clarity": 4, "coherence": 4, "correctness": 3, "concision": 2 }, "summary": "A polite, well-organized reply, but the middle sentence is padded and there is a spelling error.", "issues": [ { "quote": "In order to process the refund we will need for you to provide us with the order number.", "dimension": "concision", "note": "Wordy phrasing (\"in order to\", \"we will need for you to provide us with\").", "suggestion": "To process the refund, please send us your order number.", "start": 42, "end": 130 }, { "quote": "recieve", "dimension": "correctness", "note": "Misspelling.", "suggestion": "receive", "start": 139, "end": 146 } ] } } ``` Without `sentence_scores` and `rubric`, the response contains only `score`. ## Batch Requests[​](#batch-requests "Direct link to Batch Requests") To score many texts — a page of product descriptions, a queue of drafted replies — in one request, send `texts` (a list of 1–10 strings) instead of `text`. The same `sentence_scores` and `rubric` options apply to every item. Exactly one of `text` and `texts` must be provided. ``` curl -X POST https://api.sapling.ai/api/v1/quality \ -H "Content-Type: application/json" \ -d '{"key":"<api-key>", "texts": ["Thanks for reaching out about your order.", "Once we recieve it, the refund will be issued within 3-5 business days."]}' ``` The batch response is `{"results": [...]}` with one entry per input, in input order; each entry has exactly the single-response shape above (offsets in `sentences` and rubric `issues` are relative to that item's own text): ``` { "results": [ {"score": 4.62}, {"score": 2.71} ] } ``` Each item is scored — and, with `rubric`, evaluated — independently, and billed individually (a batch of N costs the same as N single requests). With `rubric`, the combined length of all items may be up to 20,000 characters (the same cap as a single `text`); without it, each item is independently truncated to the leading \~4,000 characters like a single request. If any item fails, the whole request returns a `502` and nothing is billed; rubrics already evaluated are cached, so a retry only re-evaluates the rest. ## Request Parameters[​](#request-parameters "Direct link to Request Parameters") **POST** to `https://api.sapling.ai/api/v1/quality` **key**: String<br />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<br />Text to score. The base score and `sentence_scores` are computed on the leading \~4,000 characters of the text (cut at a word boundary); the `rubric` evaluates the whole text and accepts up to 20,000 characters (longer text returns a `400`). Provide exactly one of `text` and `texts`. **texts**: List\[String]<br />Batch form (see [Batch Requests](#batch-requests)): 1–10 texts to score in one request, each scored — and, with `rubric`, evaluated — independently under the same options. **sentence\_scores**: Boolean<br />Whether to also return a score for each sentence. Defaults to `false`. Sentences are split on line breaks and sentence boundaries; very short fragments are merged with a neighboring sentence on the same line. **rubric**: Boolean<br />Whether to also return the LLM-judged rubric (overall and per-dimension scores, summary and issues). Defaults to `false`. Adds a few seconds of latency and is billed at the Tone rate. ## Response Parameters[​](#response-parameters "Direct link to Response Parameters") **score**: Float<br />A score from 1 to 5, with 1 being the lowest (low quality) and 5 the highest (high quality). Note that partial sentences such as `I will go to` will have a high score as the model assumes that the sequence has not yet been completed. On the other hand, `I will go to.` (including the period) will have a low score. **sentences**: List<br />Only present when `sentence_scores` is `true`. One item per sentence, in document order, each with: * `start`, `end`: character offsets of the sentence into the submitted `text` (`text[start:end]` is the sentence). Offsets are code-point indices; `null` in the rare case the sentence can't be located in the submitted text. * `text`: the sentence text. * `score`: the sentence's own 1–5 score, computed the same way as the top-level `score` over that sentence's tokens. Short sentences score a little more leniently than long ones, so compare sentences of similar length, or look at the lowest-scoring sentences first. **rubric**: Object<br />Only present when `rubric` is `true`: * `overall`: integer 1–5, the evaluator's holistic judgment (not an average of the dimensions, and independent of the model-based `score`). * `dimensions`: integer 1–5 for each of: | Dimension | What it measures | | ------------- | --------------------------------------------------------------- | | `clarity` | Easy to understand; precise, unambiguous wording | | `coherence` | Logical flow and organization; sentences and paragraphs connect | | `correctness` | Grammar, spelling, punctuation and mechanics | | `concision` | No filler, redundancy or padding | * `summary`: one or two sentences on the writing's main strengths and weaknesses. * `issues`: up to 10 concrete issues, most important first (an empty list means the evaluator found nothing to flag). Each has `quote` (a verbatim excerpt from the text), `dimension` (one of the four above), `note` (what's wrong), `suggestion` (an improved rewrite of the excerpt, or `""`), and `start`/`end` offsets of the excerpt into the submitted `text` (`null` if the excerpt couldn't be located verbatim). The rubric works for any language the underlying model handles well; the base `score` is calibrated for English.   --- # Rephrase The Paraphrase and Rephrase endpoints offer paraphrasing for particular styles. Given an input sentence, the endpoint returns output sentences that preserve meaning but use alternative phrasings or styles. [Try it out](https://sapling.ai/utilities/paraphrase?utm_campaign=apidocs) Note that while the paraphrases/rephrases retain the same general meaning, they vary in how much they differ from the input sentence. Put another way, while the syntax and terminology of the input may change, the semantics should be preserved. <!-- --> ## Rephrase POST[​](#rephrase-post "Direct link to Rephrase POST") In addition to the paraphrase endpoint, the rephrase endpoint also offers a few style transformations: * **Informal to Formal (`informal_to_formal`)**: Given a casual/informal input text, generate a more formal/professional output. You can test the endpoint [here](https://sapling.ai/utilities/informal_to_formal?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs). * **Passive to Active (`passive_to_active`)**: Generate sentence in active voice given sentence in passive voice. You can test the endpoint [here](https://sapling.ai/utilities/passive_to_active?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs). * **Shorten (`shorten`)**: Rewrite text to about half its length while preserving its meaning and tone. You can test the endpoint [here](https://sapling.ai/utilities/shorten?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs). * **Split (`sentence_split`)**: Given a long/convoluted sentence, split it into two sentences. You can test the endpoint [here](https://sapling.ai/utilities/sentence_split?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs). * **Short to Long / Expand (`expand`)**: Given some text, provide a longer version of the text while trying to (mostly) preserve the same meaning. You can test the endpoint [here](https://sapling.ai/utilities/expand?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs). `https://api.sapling.ai/api/v1/rephrase` HTTP method: `POST` ### Sample Code[​](#sample-code "Direct link to Sample Code") * cURL * JavaScript * JavaScript SDK * Python ``` curl -X POST https://api.sapling.ai/api/v1/rephrase \ -H "Content-Type: application/json" \ -d '{ "key":"<api-key>", "text":"hey wuts going on", "mapping":"informal_to_formal" }' ``` ``` import axios from 'axios'; async function run(text) { try { const response = await axios.post( 'https://api.sapling.ai/api/v1/rephrase', { key: '<api-key>', text, mapping:'informal_to_formal' }, ); const {status, data} = response; console.log({status}); console.log(JSON.stringify(data, null, 4)); } catch (err) { const { msg } = err.response.data; console.log({err: msg}); } } run('hey wuts going on'); ``` ``` // npm install @saplingai/sapling-js import { Client } from '@saplingai/sapling-js/client'; const client = new Client('<api-key>'); const { data } = await client.rephrase('hey, can u send it over', { mapping: 'informal_to_formal', num_results: 3, }); for (const { replacement } of data.results) { console.log(replacement); } ``` ``` import requests from pprint import pprint key = '<api-key>' url = 'https://api.sapling.ai/api/v1/rephrase' data = { 'key': key, 'text': 'hey wuts going on', 'mapping':'informal_to_formal' } try: response = requests.post(url, json=data) if 200 <= response.status_code < 300: resp_json = response.json() results = resp_json['results'] pprint(results) else: print('Error: ', str(response.status_code), response.text) except Exception as e: print('Error: ', e) ``` ### Sample Response[​](#sample-response "Direct link to Sample Response") ``` { "results": [ { "hash": "5c09974f-3fae-560d-9982-ba3ff7087fff", "model_version": "v20220514", "original": "hey wuts going on", "rephrase_type": "informal_to_formal", "replacement": "What is going on, please?" }, { "hash": "e8392c8f-c190-58df-84ef-887a29a78d2c", "model_version": "v20220514", "original": "hey wuts going on", "rephrase_type": "informal_to_formal", "replacement": "I am unaware of what is going on." }, { "hash": "e4dd013c-e79c-5031-ab74-4614703c3f16", "model_version": "v20220514", "original": "hey wuts going on", "rephrase_type": "informal_to_formal", "replacement": "What is going on?" }, { "hash": "8b593139-7353-50a9-8c99-e59a82d16031", "model_version": "v20220514", "original": "hey wuts going on", "rephrase_type": "informal_to_formal", "replacement": "Hey, what is happening?" } ] } ``` ### Request Parameters[​](#request-parameters "Direct link to Request Parameters") **key**: String<br />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<br />Input query to rephrase. Current maximum length is **4,000 characters**. Contact us if you need support for longer inputs. **mapping** String<br />The model to use. Available models include `paraphrase`, `informal_to_formal`, `passive_to_active`, `active_to_passive`, `sentence_split`, `expand`, and `shorten`. Additionally, there are `switch_tone` and `switch_tense` options, where **tone\_mapping** or **tense\_mapping** should also be specified, respectively. **tone\_mapping** String<br />If `mapping` is set to `switch_tone`, then `tone_mapping` will be used for the target tone. Available options include: `straightforward`, `professional`, `casual`, `confident`, `friendly`, `empathetic`, `diplomatic`, and `persuasive`. Please contact us if you want additional tones to be added. **tense\_mapping** String<br />If `mapping` is set to `switch_tense`, then `tense_mapping` will be used for the target tense. Please contact us for the list of supported tenses. Unsupported `mapping`, `tone_mapping`, and `tense_mapping` values return `400 Bad Request`. **num\_results**: Integer<br />Number of results to return. Honored by the `paraphrase`, `informal_to_formal`, `formal_to_informal`, `generate_title` and `generate_acronym` mappings; other mappings return their default number of results. The minimum is 1. The maximum is 8. Contact us if you need additional results. **diversity**: Float `[Coming soon]`<br />Ranges from 0 to 1. 0 means to not encourage diversity beyond the usual sampling. 1 means to make the outputs as diverse as possible (while still preserving semantics). ### Response Parameters[​](#response-parameters "Direct link to Response Parameters") **results**: JSON, array of rephrases. ## Paraphrase POST[​](#paraphrase-post "Direct link to Paraphrase POST") **DEPRECATED** Please instead use the Rephrase endpoint with `mapping` set to `paraphrase`. The paraphrase endpoint provides alternative phrasings of an input text while preserving meaning and intent. An example input might be: "How are you?" with the output paraphrases, "How's it going?", "What's up?", and "How are you doing?". `https://api.sapling.ai/api/v1/paraphrase` HTTP method: `POST` ### Sample Code[​](#sample-code-1 "Direct link to Sample Code") * cURL * JavaScript * Python ``` curl -X POST https://api.sapling.ai/api/v1/paraphrase \ -H "Content-Type: application/json" \ -d '{ "key":"<api-key>", "text":"How are you?", }' ``` ``` import axios from 'axios'; async function run(text) { try { const response = await axios.post( 'https://api.sapling.ai/api/v1/paraphrase', { key: '<api-key>', text, }, ); const {status, data} = response; console.log({status}); console.log(JSON.stringify(data, null, 4)); } catch (err) { const { msg } = err.response.data; console.log({err: msg}); } } run('How are you?'); ``` ``` import requests from pprint import pprint key = '<api-key>' url = 'https://api.sapling.ai/api/v1/paraphrase' data = { 'key': key, 'text': 'How are you?', } try: resp = requests.post(url, json=data) if 200 <= resp.status_code < 300: resp_json = resp.json() results = resp_json['results'] pprint(results) else: print('Error: ', str(resp.status_code), resp.text) except Exception as e: print('Error: ', e) ``` ### Sample Response[​](#sample-response-1 "Direct link to Sample Response") ``` { "results": [ { "hash": "26d70b5b-333f-52f2-9abe-7bdac7dfe95f", "model_version": "v20230414", "original": "How are you?", "rephrase_type": "paraphrase", "replacement": "What is your current state of affairs?" }, { "hash": "9db4d831-f526-5ee3-b43a-f4ad02c0e2fa", "model_version": "v20230414", "original": "How are you?", "rephrase_type": "paraphrase", "replacement": "How are you doing?" }, { "hash": "0f100133-1b07-5157-b61e-d25d13cdfa27", "model_version": "v20230414", "original": "How are you?", "rephrase_type": "paraphrase", "replacement": "What's your current state of affairs?" } ] } ``` ### Request Parameters[​](#request-parameters-1 "Direct link to Request Parameters") **key**: String<br />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<br />Input query to paraphrase. **diversity**: Float `[Coming soon]`<br />Ranges from 0 to 1. 0 means to not encourage diversity beyond the usual sampling. 1 means to make the outputs as diverse as possible (while still preserving semantics). **num\_results**: Integer<br />Number of results to return. Defaults to 5. The minimum is 1. The maximum is 8. Contact us if you need additional results. Additional results will incur additional costs. ### Response Parameters[​](#response-parameters-1 "Direct link to Response Parameters") **results**: JSON, array of paraphrases. --- # Content Safety API The content safety endpoint scores a text on seven moderation categories — toxicity, profanity, harassment, hate speech, self-harm, sexual content, and violence — each with a probability from 0 (absent) to 1 (present). Use it to moderate user-generated content, screen LLM outputs before they reach users, or route messages for human review. For simple token-level profanity flagging, see the [profanity endpoint](https://sapling.ai/docs/api/profanity.md). For determining whether text is AI-generated, see the [AI detector](https://sapling.ai/docs/api/detector.md). <!-- --> ## Sample Code[​](#sample-code "Direct link to Sample Code") * cURL * JavaScript * Python ``` curl -X POST https://api.sapling.ai/api/v1/safety \ -H "Content-Type: application/json" \ -d '{"key":"<api-key>", "text":"You are a worthless idiot and everyone hates you."}' ``` ``` import axios from 'axios'; async function run(text) { try { const response = await axios.post( 'https://api.sapling.ai/api/v1/safety', { key: '<api-key>', text, }, ); 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('You are a worthless idiot and everyone hates you.'); ``` ``` import requests from pprint import pprint response = requests.post( "https://api.sapling.ai/api/v1/safety", json={ "key": "<api-key>", "text": "You are a worthless idiot and everyone hates you." } ) if 200 <= response.status_code < 300: pprint(response.json()) else: print('Error: ', response.status_code, response.text) ``` ## Sample Response[​](#sample-response "Direct link to Sample Response") ``` { "flagged": true, "flagged_categories": ["toxicity", "harassment"], "scores": { "toxicity": 0.92, "profanity": 0.15, "harassment": 0.88, "hate_speech": 0.05, "self_harm": 0.0, "sexual": 0.0, "violence": 0.02 }, "threshold": 0.5 } ``` ## Batch Requests[​](#batch-requests "Direct link to Batch Requests") To score many short texts — a queue of chat messages, a page of reviews — in one request, send `texts` (a list of 1–10 strings) instead of `text`. The same `threshold` and `spans` options apply to every item, and the combined length of all items may be up to 20,000 characters (the same cap as a single `text`, measured on the submitted text — before HTML stripping). Exactly one of `text` and `texts` must be provided. ``` curl -X POST https://api.sapling.ai/api/v1/safety \ -H "Content-Type: application/json" \ -d '{"key":"<api-key>", "texts": ["You are a worthless idiot.", "What a lovely afternoon!"]}' ``` The batch response is `{"results": [...]}` with one entry per input, in input order; each entry has exactly the single-response shape above: ``` { "results": [ { "flagged": true, "flagged_categories": ["toxicity", "harassment"], "scores": { "toxicity": 0.92, "profanity": 0.15, "harassment": 0.88, "hate_speech": 0.05, "self_harm": 0.0, "sexual": 0.0, "violence": 0.02 }, "threshold": 0.5 }, { "flagged": false, "flagged_categories": [], "scores": { "toxicity": 0.01, "profanity": 0.0, "harassment": 0.0, "hate_speech": 0.0, "self_harm": 0.0, "sexual": 0.0, "violence": 0.0 }, "threshold": 0.5 } ] } ``` With `spans: true`, each entry's span offsets index that entry's own submitted text. 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 score, the whole request returns a `400` and nothing is billed; the items that did succeed are already cached, so a retry only re-runs the failures. ## Request Parameters[​](#request-parameters "Direct link to Request Parameters") **POST** to `https://api.sapling.ai/api/v1/safety` **key**: String<br />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<br />Text to score, up to 20,000 characters (HTML tags are stripped before scoring). Provide exactly one of `text` and `texts`. **texts**: List\[String]<br />Batch form (see [Batch Requests](#batch-requests)): 1–10 texts to score in one request, each treated exactly like `text` above, with a combined length of up to 20,000 characters. An item that is empty after HTML stripping returns a `400` naming its index. The response becomes `{"results": [...]}`, one entry per item in input order. **threshold**: Float, optional, defaults to 0.5<br />Score at or above which a category is included in `flagged_categories`. Between 0 and 1 inclusive. Raise it to only flag high-confidence violations; lower it for stricter moderation. The threshold only affects the flags — the raw `scores` are always returned. **spans**: Boolean, optional, defaults to false<br />When true, the response also includes `spans`: the specific offending passages, each with its text, character offsets, and per-passage category scores. Use it to highlight, quote, or redact just the violating passage instead of rejecting the whole text. ## Response Parameters[​](#response-parameters "Direct link to Response Parameters") **flagged**: Boolean<br />True if any category scored at or above the threshold. **flagged\_categories**: List\[String]<br />Categories that scored at or above the threshold. **scores**: Object<br />A probability from 0 to 1 for each category: | Category | Description | | ------------- | ------------------------------------------------------------------- | | `toxicity` | Rude, disrespectful, or unreasonable language overall | | `profanity` | Swear words or vulgar language | | `harassment` | Attacks, bullying, or intimidation directed at a person or group | | `hate_speech` | Attacks on protected characteristics (race, religion, gender, etc.) | | `self_harm` | Encouragement or expression of self-harm or suicide | | `sexual` | Sexually explicit content | | `violence` | Threats, incitement, or graphic descriptions of violence | **threshold**: Float<br />The threshold that was applied. **spans**: List\[Object] (only with `spans: true`)<br />The offending passages, most severe first. Each span has: * `text`: the passage, quoted from the submitted text. * `start` / `end`: character offsets of the passage in the submitted text (`text[start:end]` is the passage). Offsets count Unicode code points (Python `str` indexing) — the same unit as the [quality API](https://sapling.ai/docs/api/quality.md)'s `sentences` — not UTF-16 code units or bytes. Both are `null` when the passage cannot be located in the submitted text — for example when it sits inside stripped HTML markup. * `scores`: the category probabilities reported for this passage alone. Only categories the passage expresses are included; passages scoring below 0.25 on every category are not reported. * `flagged_categories`: the passage's categories at or above the request `threshold`. Spans with no category at or above the threshold are omitted entirely. With `spans: true` the request above returns: ``` { "flagged": true, "flagged_categories": ["toxicity", "harassment"], "scores": { "toxicity": 0.92, "profanity": 0.15, "harassment": 0.88, "hate_speech": 0.05, "self_harm": 0.0, "sexual": 0.0, "violence": 0.02 }, "threshold": 0.5, "spans": [ { "text": "You are a worthless idiot and everyone hates you.", "start": 0, "end": 49, "scores": {"toxicity": 0.92, "harassment": 0.88}, "flagged_categories": ["toxicity", "harassment"] } ] } ``` ## Tips[​](#tips "Direct link to Tips") * Scores are calibrated per category: a text can flag multiple categories at once (for example, a threat is usually both `toxicity` and `violence`). * Quoted, negated, or educational mentions of a topic score lower than direct expressions, but consider a lower threshold if your application must be conservative. * Results for identical text are cached briefly on Sapling's side, so re-checking the same content does not change scores. * Spans can overlap (a sentence and a longer passage containing it may each be reported); when highlighting, use the `start`/`end` offsets to merge or de-duplicate overlapping regions. * Because offsets are Unicode code-point indices, slice with `Array.from(text)` in JavaScript if the input may contain characters outside the Basic Multilingual Plane — its UTF-16 string indices would otherwise misalign.   --- # SCIM Provisioning Sapling Enterprise teams can connect their identity provider (IdP) to Sapling over [SCIM 2.0](https://datatracker.ietf.org/doc/html/rfc7644) to deprovision users automatically when they leave or lose access in the directory. SCIM works together with [single sign-on (SSO)](https://sapling.ai/sso?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs): SSO signs users in, and SCIM controls whether they hold a seat on your Sapling team. SCIM is available on the Enterprise plan. Contact <team@sapling.ai> to enable it. Sapling issues one SCIM token per organization. ## Connection details[​](#connection-details "Direct link to Connection details") | Setting | Value | | ----------------- | ---------------------------------------------------------------------------- | | SCIM version | SCIM 2.0 (Users and Groups) | | Base URL | `https://api.sapling.ai/scim/v2` (no trailing slash) | | Authentication | Bearer token in the `Authorization` header (`Authorization: Bearer <token>`) | | Unique identifier | `userName` (the user's email address) | | Content type | `application/scim+json` or `application/json` | Tokens do not expire. Sapling rotates a token on request: we issue a new token, you update it in your IdP, and the old token is revoked. We recommend rotating at least annually and whenever an administrator with access to the token leaves. Every SCIM request is recorded in an audit log for your organization. ## How provisioning works[​](#how-provisioning-works "Direct link to How provisioning works") A Sapling account is created **just-in-time** the first time a user signs in with SSO, and users from your verified email domain are added to your Sapling team automatically. SCIM then manages access to that team: | IdP action | Sapling result | | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Create (assign the app) | Records the user. If an account with that email is already on one of your organization's Sapling teams, it is linked immediately; otherwise it links the first time the user signs in with SSO. An `active: false` sent with the create is applied. | | Update profile | Stores the updated attributes, including a changed `userName` (for example, after an email change). The new `userName` must not belong to another user in your organization (`409` otherwise). | | Deactivate (`active: false`) or unassign | Removes the user from every Sapling team in your organization. Their seat is freed and Enterprise features stop working immediately. | | Reactivate (`active: true`) | Restores the user's memberships on your organization's Sapling teams. | | Delete | Revokes access like a deactivation and removes the user from pushed groups. The user no longer appears in SCIM reads (`GET` returns `404`). Provisioning the same `userName` again restores the record, linked to the same Sapling account. | Deleting a user through SCIM does not erase their Sapling account or data. To do that, ask your team administrator or [contact us](mailto:team@sapling.ai). ### Supported operations[​](#supported-operations "Direct link to Supported operations") | Endpoint | Methods | | -------------- | --------------------------------------------------------------------------------------- | | `/Users` | `GET` (with `filter=userName eq "<email>"`, or paged with `startIndex`/`count`), `POST` | | `/Users/{id}` | `GET`, `PUT`, `PATCH` (`active`), `DELETE` | | `/Groups` | `GET` (with `filter=displayName eq "<name>"`, or paged), `POST` | | `/Groups/{id}` | `GET`, `PUT`, `PATCH` (members add/remove/replace, `displayName`), `DELETE` | Pages return at most 100 resources. ### Attributes[​](#attributes "Direct link to Attributes") | Attribute | Required | Notes | | -------------------------------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------- | | `userName` | Yes | Must be the user's email address, the same one they use to sign in with SSO. Matched case-insensitively. | | `active` | No | Defaults to `true`. | | `name.givenName`, `name.familyName`, `displayName`, `emails`, `externalId` | No | Stored and returned. Sapling does not require them. | No custom or extension attributes are required. ### Groups[​](#groups "Direct link to Groups") Group push is supported but not required. Assigning the Sapling app to users or groups in your IdP is enough to provision and deprovision. Pushed groups are mirrored in Sapling, but they do not map to Sapling roles: team admin and manager permissions are managed in Sapling. Group membership affects access in one way. When a user is removed from a pushed group and belongs to no other pushed group, their access is revoked as if they had been deactivated. Adding a user to a pushed group restores their access. Deleting a pushed group in Sapling does not change any member's access: members are not deactivated, even if they belong to no other pushed group. Group members must already be provisioned users. A request that adds a member Sapling has not provisioned, or replaces the member list with one that includes such a member, is rejected with `400`, and none of that request's changes are applied. Removing an unknown member succeeds. Provision users (assign the app) before pushing a group that contains them. To empty a group, send `"members": []` or a `PATCH` that removes `members`. A `PUT` that omits `members` leaves the membership unchanged. If you use group push, push only the group or groups that are assigned to the Sapling app. ## Okta setup[​](#okta-setup "Direct link to Okta setup") 1. In Okta, open your Sapling app integration (the app you use for SSO), go to **General → App Settings**, and enable **SCIM provisioning**. 2. On the **Provisioning → Integration** tab, set: <!-- --> * **SCIM connector base URL**: `https://api.sapling.ai/scim/v2` * **Unique identifier field for users**: `userName` * **Supported provisioning actions**: Push New Users, Push Profile Updates, Push Groups (optional) * **Authentication Mode**: HTTP Header, using the token Sapling provided as the Bearer token 3. Click **Test Connector Configuration**, then **Save**. 4. On **Provisioning → To App**, enable **Create Users**, **Update User Attributes**, and **Deactivate Users**. 5. Make sure the Okta username or email mapped to `userName` is the address users sign in to Sapling with. 6. Assign the app to a few test users first. Confirm that they appear and that deactivating one in Okta removes them from your Sapling team. Then assign the app to your full user population. 7. Optional: set up group push on the **Push Groups** tab only after the group's members have been provisioned (step 6). ## Microsoft Entra ID (Azure AD) setup[​](#microsoft-entra-id-azure-ad-setup "Direct link to Microsoft Entra ID (Azure AD) setup") 1. In your Sapling enterprise application, open **Provisioning** and set **Provisioning Mode** to **Automatic**. 2. Set **Tenant URL** to `https://api.sapling.ai/scim/v2` and **Secret Token** to the token Sapling provided. Click **Test Connection**. 3. Under **Mappings**, open the user mapping. Map `userPrincipalName` (or `mail`) to `userName`, keep the `active` mapping (`Switch([IsSoftDeleted], ...)`), and delete the other attribute mappings (`displayName`, `name.*`, `emails`, and so on). Entra sends profile changes as `PATCH /Users/{id}` requests, and Sapling's user `PATCH` supports only `active`. Sapling does not need the other attributes. 4. Set the scope to **Sync only assigned users and groups**, and start provisioning. ## Testing[​](#testing "Direct link to Testing") Sapling does not provide a separate SCIM sandbox. Before you assign the app to everyone, we recommend testing against production with a few test users from your directory. On request, Sapling can also issue a token for a separate test organization and team. --- # Semantic Search Semantic search provides the ability to query/search over a set of documents based off the meaning of the documents (as opposed to keyword-based search). Questions?   [Contact Us](https://sapling.ai/contact?api=true\&utm_campaign=apidocs) ## Semantic Query POST[​](#semantic-query-post "Direct link to Semantic Query POST") ### Request Parameters[​](#request-parameters "Direct link to Request Parameters") ``` import requests key = '<api-key>' url = 'https://api.sapling.ai/api/v1/semsearch' data = { 'key': key, 'query': 'Who was Rin Tin Tin?', } try: resp = requests.post(url, json=data) if 200 <= resp.status_code < 300: resp_json = resp.json() results = resp_json print('Results: ', results) else: print('Error: ', str(resp.status_code), resp.text) except Exception as e: print('Error: ', e) ``` `https://api.sapling.ai/api/v1/semsearch` HTTP method: `POST` With return http status 400 with feedback in the `msg` field if the request is invalid e.g if the key is missing or incorrect. **key**: String<br />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. **query**: String<br />Query to to match against documents in the response bank. **part**: String<br />Either `prompt` or `both`. Whether to match against both documents and prompts or just prompts. ### Response Parameters[​](#response-parameters "Direct link to Response Parameters") **results**: JSON, sorted array of semantic search results of the following structure. ``` { "doc": <str>, // Text of document matching query "id": <str>, // Random string ID "score": <float> // Score of query. Higher is better. } ``` <br /> ## Create Entry POST[​](#create-entry-post "Direct link to Create Entry POST") `https://api.sapling.ai/api/v1/semsearch/create` HTTP method: `POST` ### Request Parameters[​](#request-parameters-1 "Direct link to Request Parameters") **key**: String<br />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. **team\_id**: String (UUID)<br />ID of the team to associate the entry with. **entry**: JSON<br />Of the form: ``` { "title": "Entry title", "prompt": "Entry prompt", "plain_text": 'Entry document text" } ``` ### Response Parameters[​](#response-parameters-1 "Direct link to Response Parameters") HTTP status code 201. <br /> ## Delete Entry DELETE[​](#delete-entry-delete "Direct link to Delete Entry DELETE") `https://api.sapling.ai/api/v1/semsearch/<entry_id>` Deletes entry with the ID `entry_id` assuming entry exists and user has permissions. ### Request Parameters[​](#request-parameters-2 "Direct link to Request Parameters") **\<entry\_id>**: String (UUID)<br />ID of search entry. **key**: String<br />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. ### Response Parameters[​](#response-parameters-2 "Direct link to Response Parameters") Empty, HTTP status code 204. --- # Sentiment Analysis The sentiment endpoint checks the sentiment for a given text - positive, negative, or neutral. The overall sentiment is returned as well as the sentiment for each sentence for a more fine-grained breakdown. If you're looking for specific emotions/tones, please see our [tone endpoint](https://sapling.ai/docs/api/tone.md). [Try it out](https://sapling.ai/utilities/sentiment?utm_campaign=apidocs) <!-- --> ## Sample Code[​](#sample-code "Direct link to Sample Code") * cURL * JavaScript * JavaScript SDK * Python ``` curl -X POST https://api.sapling.ai/api/v1/sentiment \ -H "Content-Type: application/json" \ -d '{"key":"<api-key>", "text":"Im really stoked about this! This was a very difficult project. Excited to see it go through!"}' ``` ``` import axios from 'axios'; async function run(text) { try { const response = await axios.post( 'https://api.sapling.ai/api/v1/sentiment', { key: '<api-key>', text, }, ); const {status, data} = response; console.log({status}); console.log(JSON.stringify(data, null, 4)); } catch (err) { const { msg } = err.response.data; console.log({err: msg}); } } run("I'm really stoked about this! This was a very difficult project. Excited to see it go through!"); ``` ``` // npm install @saplingai/sapling-js import { Client } from '@saplingai/sapling-js/client'; const client = new Client('<api-key>'); const { data } = await client.sentiment( "I'm really stoked about this! This was a very difficult project. Excited to see it go through!", ); const [[probability, label]] = data.overall; console.log(label, probability); ``` ``` import requests from pprint import pprint response = requests.post( "https://api.sapling.ai/api/v1/sentiment", json={ "key": "<api-key>", "text": "I'm really stoked about this! This was a very difficult project. Excited to see it go through!" } ) if 200 <= response.status_code < 300: pprint(response.json()) else: print('Error: ', response.status_code, response.text) ``` ## Sample Response[​](#sample-response "Direct link to Sample Response") ``` { "overall": [ [ 0.9966725875565317, "POSITIVE" ] ], "results": [ [ [ 0.9962439247756265, "POSITIVE" ] ], [ [ 0.8007598668336868, "NEGATIVE" ], [ 0.04219657927751541, "POSITIVE" ] ], [ [ 0.9941994925029576, "POSITIVE" ], [ 0.0009521204046905041, "NEUTRAL" ] ] ], "sents": [ "I'm really stoked about this!", "This was a very difficult project.", "Excited to see it go through!" ] } ``` ## Request Parameters[​](#request-parameters "Direct link to Request Parameters") **POST** to `https://api.sapling.ai/api/v1/sentiment` **key**: String<br />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<br />Text to analyze the sentiment for. ## Response Parameters[​](#response-parameters "Direct link to Response Parameters") The sentiment endpoint contains three fields: 1. `sents`: The sentences the text contains. 2. `overall`: A list of tuples of the form `(probability, sentiment)`. 3. `results`: For each sentence, a list item containing a list of tuples of the form `(probability, sentiment)`.   --- # SEO Analysis API The SEO endpoint analyzes a page or article and returns everything needed to publish it well: deterministic on-page **stats** (word, sentence and paragraph counts, reading time, readability), **keyword density** for the target keywords you pass in, the **top terms** the page actually reads as being about, and LLM-generated **suggestions** — title tags, meta descriptions, a URL slug and focus keywords — steered toward your target keywords and written in the language of the text. It is built for content teams, CMS and headless-CMS integrations, and marketing tooling that wants to check keyword coverage and generate metadata as part of a publishing workflow. For plain text statistics without keywords or suggestions, see the [statistics endpoint](https://sapling.ai/docs/api/statistics.md). [Try it out](https://sapling.ai/utilities/seo-meta-generator?utm_campaign=apidocs) ## Sample Code[​](#sample-code "Direct link to Sample Code") * cURL * JavaScript * Python * Python (Sapling client) ``` curl -X POST https://api.sapling.ai/api/v1/seo \ -H "Content-Type: application/json" \ -d '{"key":"<api-key>", "text":"A grammar checker does more than fix typos. Modern grammar checker tools flag tone, clarity, and passive voice, not just grammar.\n\nTeams use a grammar checker to keep customer messaging consistent. It works in your browser, in Google Docs, and in emails.\n\nIs a grammar checker worth it? For busy teams that spend the day writing, a grammar checker pays for itself quickly.", "keywords": ["grammar checker"]}' ``` ``` import axios from 'axios'; async function run(text) { try { const response = await axios.post( 'https://api.sapling.ai/api/v1/seo', { key: '<api-key>', text, keywords: ['grammar checker'], }, ); 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('A grammar checker does more than fix typos. Modern grammar checker tools flag tone, clarity, and passive voice, not just grammar.\n\nTeams use a grammar checker to keep customer messaging consistent. It works in your browser, in Google Docs, and in emails.\n\nIs a grammar checker worth it? For busy teams that spend the day writing, a grammar checker pays for itself quickly.'); ``` ``` import requests from pprint import pprint text = ( "A grammar checker does more than fix typos. Modern grammar checker tools flag tone, " "clarity, and passive voice, not just grammar.\n\n" "Teams use a grammar checker to keep customer messaging consistent. It works in your " "browser, in Google Docs, and in emails.\n\n" "Is a grammar checker worth it? For busy teams that spend the day writing, a grammar " "checker pays for itself quickly." ) response = requests.post( "https://api.sapling.ai/api/v1/seo", json={ "key": "<api-key>", "text": text, "keywords": ["grammar checker"], } ) 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) text = ( "A grammar checker does more than fix typos. Modern grammar checker tools flag tone, " "clarity, and passive voice, not just grammar.\n\n" "Teams use a grammar checker to keep customer messaging consistent. It works in your " "browser, in Google Docs, and in emails.\n\n" "Is a grammar checker worth it? For busy teams that spend the day writing, a grammar " "checker pays for itself quickly." ) result = client.seo(text, keywords=['grammar checker']) print(result['stats']['words'], 'words,', result['stats']['reading_time_min'], 'min read') for kw in result['keywords']: print(kw['keyword'], '|', kw['count'], 'uses |', f"{kw['density']}% |", 'early' if kw['in_first_100_words'] else 'late') print('Title:', result['suggestions']['titles'][0]) print('Description:', result['suggestions']['meta_descriptions'][0]) print('Slug:', result['suggestions']['slug']) ``` ## Sample Response[​](#sample-response "Direct link to Sample Response") ``` { "stats": { "chars": 372, "words": 63, "sentences": 6, "paragraphs": 3, "reading_time_min": 1, "flesch_reading_ease": 66.4, "flesch_kincaid_grade": 7.2 }, "keywords": [ {"keyword": "grammar checker", "count": 5, "density": 7.94, "in_first_100_words": true} ], "top_terms": [ {"term": "grammar", "count": 6}, {"term": "checker", "count": 5}, {"term": "grammar checker", "count": 5}, {"term": "teams", "count": 2} ], "suggestions": { "titles": [ "Grammar Checker Guide: What Modern Tools Catch", "How a Grammar Checker Improves Team Writing", "Is a Grammar Checker Worth It? A Practical Look" ], "meta_descriptions": [ "Learn what a modern grammar checker catches beyond typos, how teams use one to keep customer messages consistent, and whether it pays off. Try it today.", "A practical guide to grammar checkers: tone, clarity and passive voice detection, browser and Google Docs support, and the payoff for busy teams.", "Grammar checkers now flag tone and clarity as well as typos. See how teams keep messaging consistent with one, and decide if it is worth it for you." ], "slug": "grammar-checker-guide", "keywords": ["grammar checker", "grammar checker for teams", "passive voice checker"] } } ``` With `suggestions: false`, the response contains only `stats`, `keywords` and `top_terms`. ## Request Parameters[​](#request-parameters "Direct link to Request Parameters") **POST** to `https://api.sapling.ai/api/v1/seo` **key**: String<br />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<br />The page or article body, up to 20,000 characters (measured on the raw input, before any HTML is stripped). Plain text or HTML: `<script>`, `<style>`, `<noscript>` and `<template>` bodies are dropped, block-level tags become line breaks, inline tags are removed, and HTML entities are decoded. Text that is empty after stripping returns a `400`. **keywords**: List\[String], optional<br />Target keyword phrases, most important first — the first entry is treated as the primary keyword. Up to 10 phrases of 1–100 characters each; each must contain at least one letter or digit. Phrases are whitespace-trimmed and exact duplicates are dropped. Keywords are both measured in the text (see `keywords` in the response) and steered into the generated `suggestions`. **suggestions**: Boolean, optional, defaults to true<br />Whether to generate the LLM-based `suggestions` (titles, meta descriptions, slug, focus keywords). Set to `false` for a deterministic stats-only response, which is not billed. Non-boolean values return a `400`. **lang**: String, optional, defaults to "en"<br />ISO 639-1 language code of the text (a region subtag is allowed, e.g. `pt-BR`; `auto` is also accepted). This only selects the readability formulas: `flesch_reading_ease` and `flesch_kincaid_grade` are computed for `en`, `de`, `es`, `fr`, `it`, `nl`, `pl` and `ru`, and are `null` for any other code. Any well-formed code is accepted, so pages in other languages still get stats, keywords and suggestions; malformed codes return a `400`. ## Response Parameters[​](#response-parameters "Direct link to Response Parameters") **stats**: Object<br />Deterministic on-page statistics for the (stripped) text: * `chars`: character count. * `words`: word count. * `sentences`: sentence count. * `paragraphs`: paragraph count. * `reading_time_min`: estimated reading time in whole minutes, `ceil(words / 265)`. * `flesch_reading_ease`: Flesch reading-ease score, clamped to 0–100 (one decimal); higher is easier to read. * `flesch_kincaid_grade`: Flesch-Kincaid US grade level, at least 0 (one decimal). The readability formulas are the same ones used by the [statistics endpoint](https://sapling.ai/docs/api/statistics.md), selected by `lang`. Both readability values are `null` when `lang` is not one of `en`, `de`, `es`, `fr`, `it`, `nl`, `pl`, `ru`, or if the text contains no words. Sentence counting uses the English splitter regardless of `lang`. **keywords**: List\[Object]<br />One entry per submitted target keyword, in the same order (an empty list when no `keywords` were sent). Each has: * `keyword`: the phrase as submitted (trimmed). * `count`: number of case-insensitive, whole-word, non-overlapping occurrences of the phrase in the text. * `density`: `count / words × 100`, rounded to two decimals. * `in_first_100_words`: `true` if the first occurrence starts within the first 100 words of the text. **top\_terms**: List\[Object]<br />Up to 10 of the most frequent content terms (single words and two-word phrases) in the text — what the page reads as being about. English stopwords are removed, tokens must be at least 3 characters and appear at least twice, and ties are broken by first appearance. Terms are lower-cased. Each has: * `term`: the word or two-word phrase. * `count`: number of occurrences. May be empty for very short text. **suggestions**: Object<br />Only present when `suggestions` is `true` (the default). Generated in the language of the text and steered toward the submitted `keywords`: * `titles`: 1–5 suggested title tags, targeting ≤60 characters each (90 characters is the hard cap — the longest you will ever receive). * `meta_descriptions`: 1–5 suggested meta descriptions, targeting 120–155 characters each (hard cap 220). * `slug`: a lowercase ASCII kebab-case URL slug of at most 80 characters, or `null` when no ASCII slug could be derived (for example purely CJK content). * `keywords`: 0–8 suggested focus-keyword phrases, most important first. ## Errors[​](#errors "Direct link to Errors") | Status | Meaning | | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `400` | Validation error — missing or oversized `text`, text empty after stripping, or invalid `keywords` / `suggestions`. The body is `{"msg": "..."}`. | | `429` | Rate limit exceeded or key over capacity. | | `502` | `{"msg": "Unexpected error generating SEO suggestions."}` — the suggestion generation failed; safe to retry. Not billed and not cached. | ## Tips[​](#tips "Direct link to Tips") * Search engines typically display about **60 characters** of a title tag and **120–155 characters** of a meta description before truncating; the suggestions target these ranges, but check the lengths before publishing. * Put your primary keyword **first** in `keywords` — it gets the most weight in the generated titles, descriptions and slug, and its `in_first_100_words` flag tells you whether the article leads with it. * Pass `suggestions: false` for a free, fast, deterministic check of counts, keyword density and readability (for example on every save in an editor), and request suggestions only when the draft is ready. * Results for identical `text` and `keywords` are cached on Sapling's side for about three days, so re-submitting the same draft returns the same suggestions; change the text or keywords to get new ones. * Pass `lang` for non-English pages: `flesch_reading_ease` and `flesch_kincaid_grade` need per-language syllable rules and are only computed for `en`, `de`, `es`, `fr`, `it`, `nl`, `pl` and `ru` (`null` otherwise). Stats, keyword density and suggestions work for any language, and suggestions are written in the language of the text.   --- # Similar Terms Many times you may need to find terms that have a similar meaning to another term. A few examples: * You've implemented a document editing interface and want to recommend alternative wordings to the user. * You're trying to group product listings by similar attributes, some users either don't add tags or use different but synonymous tags. * You're trying to optimize ad copy and want to try replacing some words with possibly catchier alternatives. In these cases, it can be helpful to look for similar terms. The usual method for this is by using a thesaurus. Sapling offers this capability along with semantic matching to find similar terms that a thesaurus would miss. You can test out this endpoint at our utilities page as well: [Try it out](https://sapling.ai/utilities/similar-meaning?utm_campaign=apidocs) ## Similar Terms POST[​](#similar-terms-post "Direct link to Similar Terms POST") ### Sample Code[​](#sample-code "Direct link to Sample Code") <!-- --> * cURL * JavaScript * JavaScript SDK * Python ``` curl -X POST https://api.sapling.ai/api/v1/thesaurus \ -H "Content-Type: application/json" \ -d '{ "key":"<api-key>", "query":"pet" }' ``` ``` import axios from 'axios'; async function run(query) { try { const response = await axios.post( 'https://api.sapling.ai/api/v1/thesaurus', { key: '<api-key>', query, }, ); const {status, data} = response; } catch (err) { const { msg } = err.response.data; } } run('pet'); ``` ``` // npm install @saplingai/sapling-js import { Client } from '@saplingai/sapling-js/client'; const client = new Client('<api-key>'); const { data } = await client.similarTerms('pet'); for (const [similarity, term] of data.neighbors) { console.log(term, similarity); } console.log(data.synonyms); ``` ``` import requests key = '<api-key>' url = 'https://api.sapling.ai/api/v1/thesaurus' data = { 'key': key, 'query': 'pet', } try: resp = requests.post(url, json=data) if 200 <= resp.status_code < 300: resp_json = resp.json() print(resp_json) else: print('Error: ', str(resp.status_code), resp.text) except Exception as e: print('Error: ', e) ``` ### Sample Response[​](#sample-response "Direct link to Sample Response") ``` { "neighbors": [ [ 0.8128582835197449, "dog" ], [ 0.7802609205245972, "cat" ], [ 0.7524483799934387, "puppy" ], [ 0.7283433079719543, "animal" ] ], "synonyms": [ "darling", "favorite", "favourite", "dearie", "deary", "ducky", "positron_emission_tomography", "favored", "best-loved", "preferred", "preferent" ] } ``` ### Request Parameters[​](#request-parameters "Direct link to Request Parameters") `https://api.sapling.ai/api/v1/thesaurus` HTTP method: `POST` **key**: String<br />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. **query**: String<br />Term to find similar terms for. Only the first 100 characters are used; an empty query returns a 400. ### Response Parameters[​](#response-parameters "Direct link to Response Parameters") JSON containing similar terms from a lexical database (WordNet) as well as from a semantic database (terms matched using embedding similarity). The semantically similar terms are in the `neighbors` field (up to 5, most similar first). Each neighbor is a `[similarity, term]` pair, where the similarity is a floating point value from 0 to 1 (with 1 being most similar). The synonyms from the lexical database are provided in a list under the `synonyms` field. Multi-word synonyms use underscores (e.g. `domestic_dog`). Output structure: ``` { "neighbors": [ [ 0.8440492153167725, "puppy" ], [ 0.8339642882347107, "cat" ], [ 0.8266059160232544, "pup" ], [ 0.8128583431243896, "pet" ] ], "synonyms": [ "domestic_dog", "Canis_familiaris", "frump", ... ] } ``` --- # Plain Language Simplification API The simplify endpoint rewrites a text in **plain language at a reading level you choose** — for plain-language compliance (e.g. agencies covered by the US Plain Writing Act), healthcare patient communications, legal notices, customer support and education. The rewrite keeps all of the text's content, structure and language: nothing is summarized, dropped or translated. To shorten a text instead, see [summarize](https://sapling.ai/docs/api/summarize.md). The improvement is **verifiable**: when the text's language supports readability scoring, the response includes a deterministic before/after readability block (Flesch-Kincaid grade level and reading ease, the same formulas as the [statistics](https://sapling.ai/docs/api/statistics.md) endpoint), so you can see — and show your users — how far the rewrite actually moved the score. Terms that must survive the rewrite (product names, defined legal terms) can be listed in `preserve_terms`; a rewrite that drops one is refused server-side rather than returned. ## Sample Code[​](#sample-code "Direct link to Sample Code") * cURL * JavaScript * Python * Python (Sapling client) ``` curl -X POST https://api.sapling.ai/api/v1/simplify \ -H "Content-Type: application/json" \ -d '{"key":"<api-key>", "text":"The party of the first part shall remit payment within thirty (30) days of receipt of the invoice.", "reading_level": "plain", "preserve_terms": ["invoice"]}' ``` ``` import axios from 'axios'; async function run(text) { try { const response = await axios.post( 'https://api.sapling.ai/api/v1/simplify', { key: '<api-key>', text, reading_level: 'plain', preserve_terms: ['invoice'], }, ); 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('The party of the first part shall remit payment within thirty (30) days of receipt of the invoice.'); ``` ``` import requests from pprint import pprint response = requests.post( "https://api.sapling.ai/api/v1/simplify", json={ "key": "<api-key>", "text": ("The party of the first part shall remit payment within " "thirty (30) days of receipt of the invoice."), "reading_level": "plain", "preserve_terms": ["invoice"], } ) 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) result = client.simplify( 'The party of the first part shall remit payment within thirty (30) ' 'days of receipt of the invoice.', reading_level='plain', preserve_terms=['invoice'], ) print(result['simplified']) readability = result.get('readability') if readability: print(f"Grade {readability['before']['grade']} -> " f"{readability['after']['grade']}") ``` ## Sample Response[​](#sample-response "Direct link to Sample Response") ``` { "simplified": "You must pay within 30 days of getting the invoice.", "reading_level": "plain", "lang": "en", "readability": { "before": {"grade": 12.3, "ease": 42.1}, "after": {"grade": 5.8, "ease": 78.4} } } ``` ## Batch Requests[​](#batch-requests "Direct link to Batch Requests") To simplify many short texts — a set of notice paragraphs, a queue of support replies — in one request, send `texts` (a list of 1–10 strings) instead of `text`. The same `reading_level`, `preserve_terms` and `lang` (when provided) apply to every item, and the combined length of all items may be up to 5,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/simplify \ -H "Content-Type: application/json" \ -d '{"key":"<api-key>", "texts": ["The party of the first part shall remit payment.", "Utilize the aforementioned portal heretofore described."]}' ``` The batch response is `{"results": [...]}` with one entry per input, in input order; each entry has exactly the single-response shape above. With `lang` omitted, each item's language — and therefore its readability scores — is detected **per item**, so a batch may legitimately mix languages: ``` { "results": [ { "simplified": "You must pay.", "reading_level": "plain", "lang": "en", "readability": { "before": {"grade": 11.2, "ease": 47.9}, "after": {"grade": 2.1, "ease": 95.7} } }, { "simplified": "Use the portal described above.", "reading_level": "plain", "lang": "en", "readability": { "before": {"grade": 14.8, "ease": 12.3}, "after": {"grade": 4.4, "ease": 83.2} } } ] } ``` 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. ## Request Parameters[​](#request-parameters "Direct link to Request Parameters") **POST** to `https://api.sapling.ai/api/v1/simplify` **key**: String<br />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<br />The text to simplify, up to 5,000 characters. Sent to the model **exactly as submitted** — markup and line structure are preserved, not stripped — and the rewrite gives you your document back with its structure intact. Empty or whitespace-only text returns a `400`. The text is treated as untrusted data: instructions inside it are ignored and it is rewritten on its content alone. Provide exactly one of `text` and `texts`. **texts**: List\[String]<br />Batch form (see [Batch Requests](#batch-requests)): 1–10 texts to simplify in one request, each treated exactly like `text` above, with a combined length of up to 5,000 characters. An empty or whitespace-only item returns a `400` naming its index. The response becomes `{"results": [...]}`, one entry per item in input order. **reading\_level**: String, optional — defaults to `plain`<br />The target audience: * `plain` (default): plain-language style for a general audience, in the spirit of [plainlanguage.gov](https://www.plainlanguage.gov/) — short sentences, everyday words, active voice. * `elementary`: \~US grade 3–5. * `middle_school`: \~US grade 6–8. * `high_school`: \~US grade 9–10. **preserve\_terms**: List\[String], optional<br />Up to 20 terms (each up to 100 characters) the rewrite must **keep rather than paraphrase away** — product names, trademarks, defined legal terms. Enforced structurally: a rewrite that drops a term the source contains is refused with a `502` rather than returned. Matching is **case-insensitive with whitespace collapsed**, so line re-wrapping does not count as dropping a term — but by the same token a change in casing alone (e.g. `iPhone` → `Iphone`) is *not* treated as a dropped term, so where the exact capitalization of a term matters, verify it in the returned text. **lang**: String, optional — defaults to auto-detection<br />ISO 639-1 code (e.g. `en`) selecting the readability formula for the before/after scores. Omit to auto-detect (falls back to `en`); on the batch form, detection runs per item, so each result carries its own `lang`. This never changes the rewrite itself — the output always stays in the text's own language. ## Response Parameters[​](#response-parameters "Direct link to Response Parameters") **simplified**: String<br />The rewritten text: same content, same language, same document structure, at the target reading level. **reading\_level**: String<br />The reading level that was applied. **lang**: String<br />The language the readability scores were computed for — the value you passed, or the detected language when omitted. **readability**: Object, optional<br />`{"before": {"grade": ..., "ease": ...}, "after": {"grade": ..., "ease": ...}}` — deterministic Flesch-Kincaid grade level and reading ease for the submitted text and the rewrite, computed over the prose (markup excluded) with the same formulas as the [statistics](https://sapling.ai/docs/api/statistics.md) endpoint. Omitted (never an error) when the language has no supported readability formula or the text has no scoreable prose. ## Errors[​](#errors "Direct link to Errors") | Status | Meaning | | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `400` | Validation error — missing, empty or oversized `text` (or a `texts` item, named by index), both or neither of `text`/`texts`, an unknown `reading_level`, or invalid `preserve_terms`. The body is `{"msg": "..."}`. | | `401` / `403` | Missing or invalid API key. | | `429` | Rate limit exceeded or key over capacity. | | `502` | `{"msg": "Unexpected error simplifying text."}` — the rewriting model failed or the rewrite dropped a preserved term; safe to retry. Not billed and not cached (on the batch form the whole request is not billed, but items that already succeeded stay cached — see [Batch Requests](#batch-requests)). | ## Tips[​](#tips "Direct link to Tips") * **Score first, rewrite second.** The [statistics](https://sapling.ai/docs/api/statistics.md) endpoint returns the same readability scores on their own — use it to decide *which* documents need simplifying, then send only those here. * **Pick the level for your audience, not the lowest one.** `plain` is the right default for public, legal and healthcare communications; `elementary` trades more nuance away than most adult-facing content wants. * **List your defined terms.** In legal and healthcare text, capitalized defined terms (`"Subscriber"`, `"the Provider"`) and product names should go in `preserve_terms` so the rewrite can't paraphrase them away. * **Check the delta.** `readability.after.grade` tells you whether the text landed near your target; a document dense with unavoidable technical terms may need a glossary rather than a lower reading level. * Results are cached on Sapling's side for about three days, keyed on the text, reading level and preserve terms — re-running the same document returns the same rewrite; changing any of them re-generates.   --- # Service Level Agreement (“SLA”) ## Definitions[​](#definitions "Direct link to Definitions") * **Sapling** and **Company** refer to Sapling Intelligence, Inc., the provider of the Services, while **Customer** refers to the user of Services. * **Service** and **Services** Refers to the Sapling API, accessible at `https://api.sapling.ai`, as well as the API dashboard at `https://sapling.ai`. * **Monthly Uptime** is calculated by taking the percentage of time where the Services were Unavailable during a given month and subtracting that from 100%. * **Unavailable** and **Unavailability** mean that the Services were not reachable (for example, returning 400 or 404 error codes) or that the Service latency exceeds agreed upon percentile thresholds in the service agreement over a period of at least one hour and at least 100 API transactions. * **Service Credits** refers to the percentage of the monthly bill that will be credited against future payments. ## Service Commitments[​](#service-commitments "Direct link to Service Commitments") | Monthly Uptime | Service Credits | | -------------------------------------------------- | --------------- | | Less than 99.7% but equal to or greater than 99.0% | 10% | | Less than 99.0% but equal to or greater than 95.0% | 30% | | Less than 95.0% | 100% | ## Credit Requests and Payment[​](#credit-requests-and-payment "Direct link to Credit Requests and Payment") To report periods of Unavailability and receive Service Credits, please send an email to `support@sapling.ai` and include the email used to subscribe to the Services as well as evidence of the outage (for example, request logs). If we validate the request, Sapling will issue the Service Credits within one billing cycle. ## Exclusions[​](#exclusions "Direct link to Exclusions") Notwithstanding any provision in this Agreement to the contrary, no Unavailability will be deemed to have occurred if Unavailability: (i) is caused by factors outside of Sapling's reasonable control, including outages from Amazon Web Services and Force Majeure events such as natural catastrophes; (ii) results from any actions or inactions of Customer; (iii) occurs during Sapling’s scheduled maintenance hours where at least 48 hours of advance written notice was provided; (iv) occurs due to use of alpha/beta/experimental services not generally available to other customers. --- # Spellcheck For some use cases, grammar and phrase-level edits may not be necessary. If your application only requires spellcheck, the Sapling Spellcheck endpoint is a faster, cheaper solution. warning The Spellcheck endpoint does not provide grammar and phrase-level suggestions. If you're looking for a more complete solution that also checks for these as well as punctuation and other errors, consider the [Edits](https://sapling.ai/docs/api/edits-overview.md) endpoint instead. info The results you get with `neural_spellcheck` set to `true` should match those from pasting the text on [https://sapling.ai/utilities/spelling](https://sapling.ai/utilities/spelling?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs) If the results do not match, check that you're not adding additional newlines or adding unnecessary character escaping (e.g. `\\r` or `\\n`) in your request. The Spellcheck endpoint supports additional languages that the `edits` endpoint does not. For the most up-to-date full language list across Sapling products, see [sapling.ai/langs](https://sapling.ai/langs?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs). In addition, Sapling offers custom dictionaries for industries including: * [Medical / healthcare](https://sapling.ai/utilities/medical-spellcheck?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs) * Biomedical applications * Speech transcriptions (outputs of automatic speech recognition \[ASR] systems) * Localization / translations (outputs of machine translation systems) Please contact us if your language is not supported or if you're interested in the custom dictionaries. ## Request Spellcheck POST[​](#request-spellcheck-post "Direct link to Request Spellcheck POST") Sometimes you may want to only perform spellcheck without grammar edits. In that case, use the Spellcheck endpoint. `https://api.sapling.ai/api/v1/spellcheck` For those who only wish to spellcheck medical terms, use the `/api/v1/medical-spellcheck` endpoint instead. HTTP method: `POST` The Spellcheck endpoint takes the same arguments as the edits endpoint. However, it does not return an `error_type` or `general_error_type` field. Where possible, the Spellcheck endpoint will also only return edits for individual words, while the Edits endpoint will suggest phrase-level edits. ### Sample Code[​](#sample-code "Direct link to Sample Code") <!-- --> * cURL * JavaScript * Python * Python SDK ``` curl -X POST https://api.sapling.ai/api/v1/spellcheck \ -H "Content-Type: application/json" \ -d '{"key":"<api-key>", "text":"There are speling mistakes here.", "session_id": "test session"}' ``` ``` import axios from 'axios'; async function run(text) { try { const response = await axios.post( 'https://api.sapling.ai/api/v1/spellcheck', { key: '<api-key>', // replace with your API key session_id: 'test session', text, }, ); const {status, data} = response; console.log({status}); console.log(JSON.stringify(data, null, 4)); } catch (err) { const { msg } = err.response.data; console.log({err: msg}); } } run('There are speling mistakes here.'); // replace with the text you want to analyze ``` ``` import requests from pprint import pprint try: response = requests.post( "https://api.sapling.ai/api/v1/spellcheck", json={ "key": "<api-key>", "text": "There are speling mistakes here.", "session_id": "test session" } ) if 200 <= response.status_code < 300: resp_json = response.json() edits = resp_json['edits'] pprint(edits) else: print('Error: ', str(response.status_code), response.text) except Exception as e: print('Error: ', e) ``` ``` # pip: python -m pip install sapling-py # uv: uv add sapling-py from sapling import SaplingClient from pprint import pprint api_key ='<api-key>' client = SaplingClient(api_key=api_key) edits = client.edits('There are speling mistakes here.', session_id='test_session') pprint(edits) ``` ### Request Arguments[​](#request-arguments "Direct link to Request Arguments") **key**: String<br />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<br />Text to process for edits. **min\_length**: Integer<br />Minimum character length of words to suggest corrections for. Setting this too low will result in much higher false positives. The default value is `3`. **session\_id**: Optional String<br />Unique name or UUID of document or portion of text that is being checked. If not provided, defaults to current date in `YYYYMMDD` format. **multiple\_edits**: Optional Boolean<br />Default is false. If true, will return `candidates` field containing list of other potential corrections for each error. **neural\_spellcheck**: Optional Boolean<br />Default for API keys created from 2024-06-20 onwards is `true` (was previously false). If `true`, endpoint backend will use more contextual neural (GPT-style) model to make spelling suggestions. We recommend the neural model but it is currently only available for a subset of languages \[`da`, `de`, `en`, `es`, `fi`, `fr`, `id`, `it`, `nl`, `no`, `pl`, `pt`, `sv`, `tl`, `tr`] and with `multiple_edits=false`. However, it can be used in combination with the non-neural model with `multiple_edits=true`. **operations**: Optional List of Strings<br />Default is `[]`. Possible operations are described in the [Postprocess](https://sapling.ai/docs/api/postprocess.md) documentation. This will run postprocessing on outputs before returning results so you can enforce stricter capitalization, punctuation, and whitespace removal. **lang**: Optional String<br />Supported languages are: * `auto`: Autodetect language * `en`: English * `ar`: عربي * `bg`: български * `ca`: català * `cs`: čeština * `da`: dansk * `de`: Deutsch * `el`: Ελληνικά * `es`: español * `et`: eesti keel * `fa`: فارسی * `fi`: suomi * `fr`: français (`fr-fr` and `fr-ca` coming soon) * `he`: עִבְרִית * `hi`: हिन्दी * `hr`: hrvatski * `hu`: magyar nyelv * `id`: bahasa Indonesia * `is`: íslenska * `it`: italiano * `jp/ja`: 日本語 * `ko`: 한국어 * `lt`: lietuvių kalba * `lv`: latviešu valoda * `nl`: Nederlands * `no`: norsk * `pl`: polski * `pt`: português * `ro`: limba română * `ru`: русский * `sk`: slovenčina * `sq`: shqip * `sr`: srpski * `sv`: svenska * `th`: ภาษาไทย (spelling suggestions are paused while the Thai word list is rebuilt) * `tl`: Tagalog / ᜆᜄᜎᜓᜄ᜔ * `tr`: Türkçe * `uk`: Українська мова * `vi`: Tiếng Việt * `zh`: 中文 **variety**: Optional String<br /> Supported Varieties This option is currently available for: English, Chinese (中文). Specifies regional English/Chinese spelling options. The setting defaults to the configuration in the Sapling dashboard. Supported varieties: English: * `us-variety`: American English * `gb-variety`: British English * `au-variety`: Australian English * `ca-variety`: Canadian English * `null-variety`: Don’t suggest any changes based on English variety Chinese (中文) * `s-variety`: Simplified Chinese * `t-variety`: Traditional Chinese **team\_id**: Optional String (UUID or `user_only`)<br /> * If `team_id` is a UUID: apply user settings plus that specific team's settings (dictionary, custom mappings, etc.). Returns HTTP 403 if the team does not exist or the user is not a member of that team. * If `team_id` is `user_only`: apply only user settings (including user dictionary), with no team settings. * If omitted: apply user settings plus settings merged from all teams the user belongs to. **auto\_apply**: Optional Boolean<br />Default is false. If true, the response has an extra `applied_text` field containing the text with the edits applied. ### Response Parameters[​](#response-parameters "Direct link to Response Parameters") The Spellcheck endpoint returns JSON with the same `edits` array as the [Edits](https://sapling.ai/docs/api/edits-overview.md#response-parameters) endpoint. Spelling edits have no `error_type` or `general_error_type` fields: ``` { "edits": [ { "id": "cd8e3bd8-8b4c-5b3a-ae52-9b8b5bce8a22", "sentence": "There are speling mistakes here.", "sentence_start": 0, "start": 10, "end": 17, "replacement": "spelling" } ] } ``` The following fields are present only in the case described: **lang**: String<br />The detected language code, when the request sent `lang: "auto"`. **candidates**: Object<br />When `multiple_edits` is `true`: each misspelled word mapped to its ranked candidate corrections, e.g. `{"speling": ["spelling", "spieling"]}`. The object is empty when the neural model served the request. **applied\_text**: String<br />When `auto_apply` is `true`: the text with every edit applied. A `warning` field is added if the text contains HTML tags. **degraded**: Object<br />On a partial result only: `{"sentences_degraded": <int>, "sentences_total": <int>, "reason": <str>}`. Some sentences could not be fully processed in time and may be missing some or all of their edits. They aren't identified, so retry the whole request for a complete result. See the [Edits response](https://sapling.ai/docs/api/edits-overview.md#response-parameters) for details. When no sentence can be checked, the endpoint answers `503` with a `Retry-After` header instead. --- # Statistics The Statistics endpoint computes simple statistics for a given text document. These statistics include word and sentence counts as well as Flesch-Kincaid score. tip This endpoint is free of charge. ## Statistics POST[​](#statistics-post "Direct link to Statistics POST") ### Sample Code[​](#sample-code "Direct link to Sample Code") * cURL * JavaScript * JavaScript SDK * Python ``` curl -X POST https://api.sapling.ai/api/v1/statistics \ -H "Content-Type: application/json" \ -d '{"key":"<api-key>", "text":"Hi, how are you doing?"}' ``` ``` import axios from 'axios'; async function run(text) { try { const response = await axios.post( 'https://api.sapling.ai/api/v1/statistics', { "key": '<api-key>', text, }, ); const {status, data} = response; console.log({status}); console.log(JSON.stringify(data, null, 4)); } catch (err) { const { msg } = err.response.data; console.log({err: msg}); } } run('Hi, how are you doing?'); ``` ``` // npm install @saplingai/sapling-js import { Client } from '@saplingai/sapling-js/client'; const client = new Client('<api-key>'); const { data } = await client.statistics('Your text here.', { lang: 'en' }); console.log(data.words, data.reading_time_min, data.readability); ``` ``` import requests from pprint import pprint response = requests.post( "https://api.sapling.ai/api/v1/statistics", json={ "key": "<api-key>", "text": "Hi, how are you doing?" } ) if 200 <= response.status_code < 300: pprint(response.json()) else: print('Error: ', response.status_code, response.text) ``` ### Request Parameters[​](#request-parameters "Direct link to Request Parameters") `https://api.sapling.ai/api/v1/statistics` HTTP method: `POST` The Statistics API POST endpoint takes JSON parameters documented below: **key**: String<br />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<br />Text to compute statistics for. The current length limit is 50,000 characters; contact us for longer lengths or use our [chunking API](https://sapling.ai/docs/api/chunk). **lang**: String<br />Language to compute statistics for. Currently supported languages: `en`, `de`, `fr`, `it`, `nl`, `pl`, `ru`, `ar`. Contact us if you need support for other languages. ### Response Parameters[​](#response-parameters "Direct link to Response Parameters") The Statistics POST endpoint returns JSON of the following format: ``` { "chars": 22, "readability": 0.5, // Flesch-Kincaid readability grade "readability_scores": { "automated_readability_index": -2.1, "coleman_liau_index": -3.17, "dale_chall_readability_score": 10.2, "difficult_words": 1, "flesch_kincaid_grade": 0.5, "flesch_reading_ease": 100.24, "gunning_fog": 2.0, "linsear_write_formula": 1.5, "mcalpine_eflaw": 9.0, "smog_index": 0.0, "spache_readability": 3.26, "text_standard": "1st and 2nd grade" }, "reading_time_min": 1, "reading_time_sec": 1.13, "sentences": 1, "words": 5 } ```   --- # 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](https://sapling.ai/docs/api/edits-overview.md) endpoint; for a generic writing-quality rubric, see [quality](https://sapling.ai/docs/api/quality.md). You can also define your house style once as a [saved ruleset](#saved-rulesets) 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[​](#sample-code "Direct link to 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[​](#sample-response "Direct link to 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[​](#batch-requests "Direct link to 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[​](#saved-rulesets "Direct link to 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[​](#request-parameters "Direct link to Request Parameters") **POST** to `https://api.sapling.ai/api/v1/styleguide` **key**: String<br />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<br />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]<br />Batch form (see [Batch Requests](#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]<br />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<br />The name of a [saved ruleset](#saved-rulesets) 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[​](#response-parameters "Direct link to Response Parameters") **violations**: List\[Object]<br />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 (`text` equals the exact slice `submitted_text[start:end]`). Offsets count Unicode code points (Python `str` indexing) — not UTF-16 code units or bytes — so in JavaScript slice with `Array.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 are `null` in the rare case the passage cannot be located in the submitted text; the violation is still real and `text` still 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]<br />The checked rule names, normalized (trimmed, internal whitespace collapsed), in the order submitted. **compliant**: Boolean<br />`true` exactly when `violations` is empty. ## Errors[​](#errors "Direct link to 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](#batch-requests)). | ## Tips[​](#tips "Direct link to Tips") * **Describe your rules.** Plain names work for common conventions (`active voice`, `no exclamation marks`), but a `description` is 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.** `text` is always the exact `start:end` slice of what you sent, so you can splice `suggestion` over `[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 over `Array.from(text)` rather than the raw string when the input may contain non-BMP characters like emoji. Skip violations whose offsets are `null`. * **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](https://sapling.ai/docs/api/custom-mappings.md) 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.   --- # Summarize API The summarize endpoint condenses a longer document — an article, call transcript, meeting notes, a whole webpage or an email thread — into a prose summary plus a list of key points. Input may be plain text or HTML: markup is stripped so you can pass a page body as-is. Good summarization balances two factors: **coverage** (the salient points of the source must survive) and **compression** (the output must be significantly more digestible than the input). The `length` option lets you pick where on that trade-off the summary should sit. You can test the summarize endpoint [here](https://sapling.ai/utilities/summarize?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs). <!-- --> ## Sample Code[​](#sample-code "Direct link to Sample Code") * cURL * JavaScript * Python ``` curl -X POST https://api.sapling.ai/api/v1/summarize \ -H "Content-Type: application/json" \ -d '{"key":"<api-key>", "text":"Hi, I was charged twice for my order this month. The first charge posted on the 3rd and a second identical charge posted on the 5th. Please refund the duplicate charge to my original payment method.", "length":"short"}' ``` ``` import axios from 'axios'; async function run(text) { try { const response = await axios.post( 'https://api.sapling.ai/api/v1/summarize', { key: '<api-key>', text, length: 'short', }, ); 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('Hi, I was charged twice for my order this month. The first charge posted on the 3rd and a second identical charge posted on the 5th. Please refund the duplicate charge to my original payment method.'); ``` ``` import requests from pprint import pprint response = requests.post( "https://api.sapling.ai/api/v1/summarize", json={ "key": "<api-key>", "text": "Hi, I was charged twice for my order this month. The first charge posted on the 3rd and a second identical charge posted on the 5th. Please refund the duplicate charge to my original payment method.", "length": "short" } ) if 200 <= response.status_code < 300: pprint(response.json()) else: print('Error: ', response.status_code, response.text) ``` ## Sample Response[​](#sample-response "Direct link to Sample Response") ``` { "summary": "A customer was charged twice for one order and asks for the duplicate charge to be refunded.", "key_points": [ "Two identical charges posted for a single order, on the 3rd and the 5th.", "The customer requests a refund of the duplicate to the original payment method." ], "length": "short", "result": "A customer was charged twice for one order and asks for the duplicate charge to be refunded." } ``` ## Batch Requests[​](#batch-requests "Direct link to Batch Requests") To summarize several documents — a queue of support tickets, a folder of meeting notes — in one request, send `texts` (a list of 1–10 strings) instead of `text`. The same `length` option applies to every item, and the combined length of all items may be up to 20,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/summarize \ -H "Content-Type: application/json" \ -d '{"key":"<api-key>", "texts": ["First document ...", "Second document ..."], "length": "short"}' ``` The batch response is `{"results": [...]}` with one entry per input, in input order; each entry has exactly the single-response shape above: ``` { "results": [ { "summary": "...", "key_points": ["...", "..."], "length": "short", "result": "..." }, { "summary": "...", "key_points": ["..."], "length": "short", "result": "..." } ] } ``` 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. An item that is empty after HTML stripping returns a `400` naming its index. If any item fails to summarize, 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. ## File Uploads[​](#file-uploads "Direct link to File Uploads") Reports, papers, contracts and slide decks usually arrive as files. Instead of extracting their text yourself, you can upload the document to the file endpoint. It reads the document's text and summarizes it exactly like `text`. **POST** to `https://api.sapling.ai/api/v1/file-summarize` as `multipart/form-data`. * **`file`** (required): the document. The format is chosen by file extension: `.pdf`, `.docx`, `.pptx`, `.odt`, `.txt`, `.md`, `.html` or `.htm`. Uploads are limited to 30MB; a larger file returns `413`. * **`jsonParams`** (optional): a JSON object with the options below, sent as a file part or as a plain form field: * `length`: optional, the same values as on `/api/v1/summarize` (defaults to `medium`). * `key`: optional when you send the key as a bearer token in the `Authorization` header. * `return_usage`: optional boolean; `true` adds the character count submitted for metering to the response. It is counted before repeat-request deduplication, so it can exceed what you are billed (see [Per-Request Usage](https://sapling.ai/docs/api/api-reporting.md#per-request-usage)). Any other option returns a `400`, so a misspelled `"lenght"` is never silently read as the default. ``` curl -X POST https://api.sapling.ai/api/v1/file-summarize \ -H "Authorization: Bearer <api-key>" \ -F "file=@report.pdf" \ -F 'jsonParams={"length": "short"}' ``` ``` import json import requests from pprint import pprint with open("report.pdf", "rb") as f: response = requests.post( "https://api.sapling.ai/api/v1/file-summarize", headers={"Authorization": "Bearer <api-key>"}, files={"file": f}, data={"jsonParams": json.dumps({"length": "short"})}, ) if 200 <= response.status_code < 300: pprint(response.json()) else: print('Error: ', response.status_code, response.text) ``` The response has the single-request shape (`summary`, `key_points`, `length`, `result`) plus a `document` object: ``` { "summary": "Revenue rose 12% on subscription growth, while support costs fell ...", "key_points": ["...", "..."], "length": "short", "result": "Revenue rose 12% on subscription growth, while support costs fell ...", "document": { "file_type": "pdf", "characters": 48210, "characters_used": 19996, "truncated": true } } ``` * **`file_type`**: the extension the document was read as. * **`characters`**: the length of the text extracted from the file. * **`characters_used`**: how much of that text was summarized. This is also what is billed. * **`truncated`**: `true` when the text was longer than 20,000 characters (the same cap as `text`). Instead of returning a `400`, only the first 20,000 characters are read, cut back to a word boundary, so the summary covers only that part of the document. To summarize a later section, extract its text yourself and send it as `text`. Billing and caching are the same as for `text`: the same text sent either way shares one cache entry. An `.html` or `.htm` file is converted to text when it is read; text read from any file is then used as-is rather than markup-stripped again, so a PDF whose text literally contains `a <b` keeps it. Problems with the file itself return `{"error": "<reason>"}`. A `400` here means one of these: * the `file` part is missing * the extension is unsupported * the document is empty, encrypted, corrupt or has no extractable text Problems with `jsonParams` or `length` return `{"msg": "..."}`. A summarization failure returns a `502` and is not billed. Upload requests are limited to 60 per minute per account; the limit is shared with the other upload endpoints. ## Request Parameters[​](#request-parameters "Direct link to Request Parameters") **POST** to `https://api.sapling.ai/api/v1/summarize` **key**: String<br />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<br />Input document to summarize, up to 20,000 characters. May be plain text or HTML (a webpage or email body): markup is stripped while paragraph and heading boundaries are preserved. Provide exactly one of `text` and `texts`. **texts**: List\[String]<br />Batch form (see [Batch Requests](#batch-requests)): 1–10 documents to summarize in one request, each treated exactly like `text` above, with a combined length of up to 20,000 characters. The response becomes `{"results": [...]}`, one entry per item in input order. **length**: String, optional, defaults to `medium`<br />Target summary length: `short` (a sentence or two), `medium`, or `long` (a fuller digest of the source). The same option applies to every item of a batch. ## Response Parameters[​](#response-parameters "Direct link to Response Parameters") **summary**: String<br />The prose summary. **key\_points**: List\[String]<br />The source's salient points as short standalone statements, most important first. **length**: String<br />The length option the summary was generated with. **result**: String<br />Mirrors `summary`. This is the endpoint's original response key, kept for backward compatibility — prefer `summary` in new integrations. ## Tips[​](#tips "Direct link to Tips") * Results for identical text and options are cached briefly on Sapling's side, so re-requesting the same document is fast and does not produce a different summary. * A summarization failure returns a `502` and is neither cached nor billed — retry the request. * Rate-limited requests return a `429` with a `Retry-After` header giving the honest backoff in seconds. --- # Superuser Management Access For changing the team directly through API, the key holder must be an superuser for the deployment. ## User CREATE[​](#user-create "Direct link to User CREATE") API key holder must be an superuser for the deployment. Sapling accounts can be created directly through this API. Caution should be taken around how passwords are generated or stored. ### Request Parameters[​](#request-parameters "Direct link to Request Parameters") ``` import requests key = '<api-key>' url = 'https://api.sapling.ai/api/v1/create_user' data = { 'key': key, 'email': '<user_email>', 'first_name': '<first_name>', 'last_name': '<last_name>', 'password': '<password>', } try: resp = requests.post(url, json=data) if 200 <= resp.status_code < 300: print('Success') else: print('Error: ', str(resp.status_code), resp.text) except Exception as e: print('Error: ', e) ``` `https://api.sapling.ai/api/v1/create_user` HTTP method: `POST` **key**: String<br />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. **email**: String<br />Email of user to create an account for **password**: String<br />Password of new user account. Minimum length of 8 characters. Always salt and hash user created passwords when storing. **first\_name**: String<br />First or given name of user **last\_name**: String<br />Last or family name of user **email\_confirmed**: Optional Boolean<br />Defaults to false. If true, user account will be created with full account privileges. If false or not specified, an email will be send with a unique link to the email specified, and the account owner will need to verify their email. ### Response Parameters[​](#response-parameters "Direct link to Response Parameters") HTTP status code 201. ## User DELETE[​](#user-delete "Direct link to User DELETE") API key holder must be an superuser for the deployment. ### Request Parameters[​](#request-parameters-1 "Direct link to Request Parameters") ``` import requests key = '<api-key>' url = 'https://api.sapling.ai/api/v1/delete_user' data = { 'key': key, 'email': '<user_email>', } try: resp = requests.post(url, json=data) if 200 <= resp.status_code < 300: print('Success') else: print('Error: ', str(resp.status_code), resp.text) except Exception as e: print('Error: ', e) ``` `https://api.sapling.ai/api/v1/delete_user` HTTP method: `POST` **key**: String<br />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. **email**: String<br />Email of user to delete from deployment ### Response Parameters[​](#response-parameters-1 "Direct link to Response Parameters") Empty, HTTP status code 204. ## Non-team Users GET[​](#non-team-users-get "Direct link to Non-team Users GET") API key holder must be an superuser for the deployment. ### Request Parameters[​](#request-parameters-2 "Direct link to Request Parameters") ``` import requests key = '<api-key>' url = f'https://api.sapling.ai/api/v1/user/nonteam?key={key}' try: resp = requests.get(url) if 200 <= resp.status_code < 300: print('Success') else: print('Error: ', str(resp.status_code), resp.text) except Exception as e: print('Error: ', e) ``` `https://api.sapling.ai/api/v1/user/nonteam?key=<key>` HTTP method: `GET` **key**: String<br />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. ### Response Parameters[​](#response-parameters-2 "Direct link to Response Parameters") JSON array of User Entries not yet associated with a team, under key “users” Non-Team User Entry Structure: ``` { "login_email": <str>, "first_name": <str>, "last_name": <str>, "created_at": <str, date> // e.g. "Wed, 07 Oct 2020 21:15:47 GMT" } ``` ## User Count GET[​](#user-count-get "Direct link to User Count GET") API key holder must be an superuser for the deployment. ### Request Parameters[​](#request-parameters-3 "Direct link to Request Parameters") ``` import requests key = '<api-key>' url = f'https://api.sapling.ai/api/v1/user/count?key={key}' try: resp = requests.get(url) if 200 <= resp.status_code < 300: resp_json = resp.json() print('Counts: ', resp_json) else: print('Error: ', str(resp.status_code), resp.text) except Exception as e: print('Error: ', e) ``` `https://api.sapling.ai/api/v1/user/count?key=<key>` HTTP method: `GET` **\<key>**: String<br />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. ### Response Parameters[​](#response-parameters-3 "Direct link to Response Parameters") **user\_count**: Integer<br />Count of total users on deployment **team\_user\_count**: Integer<br />Count of users assigned to a team --- # Team Custom Mappings API key holder must be an admin for the team to access a team's dictionary. Custom Mappings can be used to enforce specific corrections or capitalization for the following purposes: * Brand voice: making sure internal slang is not used in outward communcation * Compliance: if there is a specific way to communicate an issue * Jargon, product names, specific capitalization This is the team-wide counterpart to [Custom Mappings](https://sapling.ai/docs/api/custom-mappings.md). If you are not managing a team, you'll want to use Custom Mappings instead. Team custom mappings will apply to all members of the team. See [Team Management](https://sapling.ai/docs/api/team-user-management.md) for getting the IDs for a particular team. ## Custom Mapping GET[​](#custom-mapping-get "Direct link to Custom Mapping GET") ### Request Parameters[​](#request-parameters "Direct link to Request Parameters") ``` import requests key = '<api-key>' team_id = '<TEAM_ID>' url = f'https://api.sapling.ai/api/v1/team/{team_id}/custom_mapping?key={key}' try: resp = requests.get(url) if 200 <= resp.status_code < 300: resp_json = resp.json() print('Custom mapping: ', resp_json) else: print('Error: ', str(resp.status_code), resp.text) except Exception as e: print('Error: ', e) ``` `https://api.sapling.ai/api/v1/team/<team_id>/custom_mapping?key=<key>` HTTP method: `GET` **\<team\_id>**: String (UUID)<br />ID of a Sapling team. **\<key>**: String<br />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. ### Response Parameters[​](#response-parameters "Direct link to Response Parameters") JSON array of custom mapping entries under key `custom_mappings`. Custom mapping entry structure: ``` { "id": <str, UUID>, // Opaque id, used for deletion "entry": <str>, // Word to match "mapping": <str>, // Word to suggest as replacement "description": <str|null>, // Shown with the suggestion; "" or null when none "case_sensitive": <bool> // true or false "created_at": <str, date> // e.g. "Wed, 07 Oct 2020 21:15:47 GMT" "updated_at": <str, date> // e.g. "Wed, 07 Oct 2020 21:15:47 GMT" } ``` ## Custom Mapping POST[​](#custom-mapping-post "Direct link to Custom Mapping POST") ### Request Parameters[​](#request-parameters-1 "Direct link to Request Parameters") ``` import requests key = '<api-key>' team_id = '<TEAM_ID>' url = f'https://api.sapling.ai/api/v1/team/{team_id}/custom_mapping' data = { 'key': key, 'entry': 'foobazbar', 'mapping': 'foobarbaz', 'case_sensitive': False, } try: resp = requests.post(url, json=data) if 200 <= resp.status_code < 300: resp_json = resp.json() print('Entry: ', resp_json) else: print('Error: ', str(resp.status_code), resp.text) except Exception as e: print('Error: ', e) ``` `https://api.sapling.ai/api/v1/team/<team_id>/custom_mapping` HTTP method: `POST` **\<team\_id>**: String (UUID)<br />ID of a Sapling team. **key**: String<br />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. **entry**: String<br />Word to match There are currently a couple types of regular expression-like options: * `^` and `$` match the start and end of a sentence, respectively. Example: `^the` will only match `the` at the start of a sentence and can be used to enforce capitalization. * `#` will match a number; example: `Gate #` will match in `Boarding is at Gate 5`. **mapping**: String<br />Word to suggest for a match. You can use `#` in the resulting expression to substitute a number back in. **case\_sensitive**: Optional Boolean<br />Defaults to false. If not case-sensitive, lower case, title case and all-caps will match. **description**: Optional String<br />The description will be shown when the edit is suggested to the end user. If a description field is specified, the user will not have the option to ignore the edit. ### Response Parameters[​](#response-parameters-1 "Direct link to Response Parameters") JSON, Created Custom Mapping Entry Custom Mapping Entry Structure: ``` { "id": <str, UUID>, // Opaque id, used for deletion "entry": <str>, // Word to match "mapping": <str>, // Word to suggest as replacement "description": <str|null>, // Shown with the suggestion; "" or null when none "case_sensitive": <bool> // true or false "created_at": <str, date> // e.g. "Wed, 07 Oct 2020 21:15:47 GMT" "updated_at": <str, date> // e.g. "Wed, 07 Oct 2020 21:15:47 GMT" } ``` ## Custom Mapping DELETE[​](#custom-mapping-delete "Direct link to Custom Mapping DELETE") ### Request Parameters[​](#request-parameters-2 "Direct link to Request Parameters") ``` import requests key = '<api-key>' team_id = '<TEAM_ID>' entry_id = '<CUSTOM_MAPPING_ID>' url = f'https://api.sapling.ai/api/v1/team/{team_id}/custom_mapping/{entry_id}' data = { 'key': key, } try: resp = requests.delete(url, json=data) if 200 <= resp.status_code < 300: print('Success') else: print('Error: ', str(resp.status_code), resp.text) except Exception as e: print('Error: ', e) ``` `https://api.sapling.ai/api/v1/team/<team_id>/custom_mapping/<custom_mapping_id>` HTTP method: `DELETE` **\<team\_id>**: String (UUID)<br />ID of a Sapling team. **\<custom\_mapping\_id>**: String (UUID)<br />ID referring to a custom mapping entry. **key**: String<br />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. ### Response Parameters[​](#response-parameters-2 "Direct link to Response Parameters") Empty, HTTP status code 204. # Team Custom Filters Team Custom Filters allow you to ignore specific edits that match defined patterns during the editing process for all team members. This is useful for suppressing certain types of corrections that your team doesn't want to see suggested. API key holder must be an admin for the team to access a team's custom filters. ## Team Custom Filter GET[​](#team-custom-filter-get "Direct link to Team Custom Filter GET") ### Request Parameters[​](#request-parameters-3 "Direct link to Request Parameters") ``` import requests key = '<api-key>' team_id = '<TEAM_ID>' url = f'https://api.sapling.ai/api/v1/team/{team_id}/custom_filter?key={key}' try: resp = requests.get(url) if 200 <= resp.status_code < 300: resp_json = resp.json() print('Team custom filters: ', resp_json) else: print('Error: ', str(resp.status_code), resp.text) except Exception as e: print('Error: ', e) ``` `https://api.sapling.ai/api/v1/team/<team_id>/custom_filter?key=<key>` HTTP method: `GET` **\<team\_id>**: String (UUID)<br />ID of a Sapling team. **\<key>**: String<br />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. ### Response Parameters[​](#response-parameters-3 "Direct link to Response Parameters") JSON array of custom filter entries under key `custom_filters`. Custom filter entry structure: ``` { "id": <str, UUID>, // Opaque id, used for deletion "created_at": <str, date>, // e.g. "Wed, 07 Oct 2020 21:15:47 GMT" "updated_at": <str, date>, // e.g. "Wed, 07 Oct 2020 21:15:47 GMT" "input": <str>, // Input text to match (can be empty string for wildcards) "output": <str>, // Output text to match (can be empty string for wildcards) "case_sensitive": <bool> // true or false } ``` ## Team Custom Filter POST[​](#team-custom-filter-post "Direct link to Team Custom Filter POST") ### Request Parameters[​](#request-parameters-4 "Direct link to Request Parameters") ``` import requests key = '<api-key>' team_id = '<TEAM_ID>' url = f'https://api.sapling.ai/api/v1/team/{team_id}/custom_filter' data = { 'key': key, 'inp': 'Patient', 'out': 'The patient', 'case_sensitive': False, } try: resp = requests.post(url, json=data) if 200 <= resp.status_code < 300: resp_json = resp.json() print('Team filter created: ', resp_json) else: print('Error: ', str(resp.status_code), resp.text) except Exception as e: print('Error: ', e) ``` `https://api.sapling.ai/api/v1/team/<team_id>/custom_filter` HTTP method: `POST` **\<team\_id>**: String (UUID)<br />ID of a Sapling team. **key**: String<br />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. **inp**: String<br />Input text to match. Can be empty string for wildcards. **out**: String<br />Output text to match. Can be empty string for wildcards. **case\_sensitive**: Boolean<br />Whether the filter should be case sensitive. Defaults to false. ### Response Parameters[​](#response-parameters-4 "Direct link to Response Parameters") JSON, Created Custom Filter Entry Custom Filter Entry Structure: ``` { "id": <str, UUID>, // Opaque id, used for deletion "created_at": <str, date>, // e.g. "Wed, 07 Oct 2020 21:15:47 GMT" "updated_at": <str, date>, // e.g. "Wed, 07 Oct 2020 21:15:47 GMT" "input": <str>, // Input text to match "output": <str>, // Output text to match "case_sensitive": <bool> // true or false } ``` ## Team Custom Filter DELETE[​](#team-custom-filter-delete "Direct link to Team Custom Filter DELETE") ### Request Parameters[​](#request-parameters-5 "Direct link to Request Parameters") ``` import requests key = '<api-key>' team_id = '<TEAM_ID>' filter_id = '<CUSTOM_FILTER_ID>' url = f'https://api.sapling.ai/api/v1/team/{team_id}/custom_filter/{filter_id}' data = { 'key': key, } try: resp = requests.delete(url, json=data) if 200 <= resp.status_code < 300: print('Success') else: print('Error: ', str(resp.status_code), resp.text) except Exception as e: print('Error: ', e) ``` `https://api.sapling.ai/api/v1/team/<team_id>/custom_filter/<team_custom_filter_id>` HTTP method: `DELETE` **\<team\_id>**: String (UUID)<br />ID of a Sapling team. **\<team\_custom\_filter\_id>**: String (UUID)<br />ID of the custom filter to delete. **key**: String<br />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. ### Response Parameters[​](#response-parameters-5 "Direct link to Response Parameters") Empty, HTTP status code 204. --- # Team Dictionary API key holder must be an admin for the team to access a team's dictionary. Add terms such as jargon or product names to the custom dictionary so that Sapling will not give suggestions for them. A team dictionary will apply to all members of the team. See [Team Management](https://sapling.ai/docs/api/team-user-management.md) for getting the IDs for a particular team. ## Dictionary GET[​](#dictionary-get "Direct link to Dictionary GET") ### Request Parameters[​](#request-parameters "Direct link to Request Parameters") ``` import requests key = '<api-key>' url = f'https://api.sapling.ai/api/v1/team/<team_id>/dictionary?key={key}' try: resp = requests.get(url) if 200 <= resp.status_code < 300: resp_json = resp.json() print('Dictionary entries: ', resp_json) else: print('Error: ', str(resp.status_code), resp.text) except Exception as e: print('Error: ', e) ``` `https://api.sapling.ai/api/v1/team/<team_id>/dictionary?key=<key>` HTTP method: `GET` **\<team\_id>**: String (UUID)<br />ID of a Sapling team. **\<key>**: String<br />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. ### Response Parameters[​](#response-parameters "Direct link to Response Parameters") JSON array of dictionary entries. Dictionary entry structure: ``` { "id": <str, UUID>, // Opaque id, used for deletion "entry": <str>, // Dictionary entry (case-sensitive) "created_at": <str, date> // e.g. "Wed, 07 Oct 2020 21:15:47 GMT" "updated_at": <str, date> // e.g. "Wed, 07 Oct 2020 21:15:47 GMT" } ``` ## Dictionary POST[​](#dictionary-post "Direct link to Dictionary POST") ### Request Parameters[​](#request-parameters-1 "Direct link to Request Parameters") ``` import requests key = '<api-key>' team_id = '<TEAM_ID>' url = f'https://api.sapling.ai/api/v1/team/{team_id}/dictionary' data = { 'key': key, 'entry': 'foobarbaz', } try: resp = requests.post(url, json=data) if 200 <= resp.status_code < 300: resp_json = resp.json() print('Entry: ', resp_json) else: print('Error: ', str(resp.status_code), resp.text) except Exception as e: print('Error: ', e) ``` `https://api.sapling.ai/api/v1/team/<team_id>/dictionary` HTTP method: `POST` **\<team\_id>**: String (UUID)<br />ID referring to a Sapling team **key**: String<br />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. **entry**: String<br />Case Sensitive word **case\_sensitive**: Optional Boolean<br />Defaults to false. If not case-sensitive, lower case, title case and all-caps will match. ### Response Parameters[​](#response-parameters-1 "Direct link to Response Parameters") JSON, the response contains the created dictionary entry Dictionary Entry Structure: ``` { "id": <str, UUID>, // Opaque id, used for deletion "entry": <str>, // Word in dictionary "case_sensitive": <bool>, // Whether or not entry is case sensitive "created_at": <str, date> // e.g. "Wed, 07 Oct 2020 21:15:47 GMT" "updated_at": <str, date> // e.g. "Wed, 07 Oct 2020 21:15:47 GMT" } ``` ## Dictionary DELETE[​](#dictionary-delete "Direct link to Dictionary DELETE") ### Request Parameters[​](#request-parameters-2 "Direct link to Request Parameters") ``` import requests key = '<api-key>' team_id = '<TEAM_ID>' entry_id = '<DICTIONARY_ENTRY_ID >' url = f'https://api.sapling.ai/api/v1/team/{team_id}/dictionary/{entry_id}' data = { 'key': key, } try: resp = requests.delete(url, json=data) if 200 <= resp.status_code < 300: print('Success') else: print('Error: ', str(resp.status_code), resp.text) except Exception as e: print('Error: ', e) ``` HTTP method: `DELETE` `https://api.sapling.ai/api/v1/team/<team_id>/dictionary/<dictionary_entry_id>` **\<team\_id>**: String (UUID)<br />ID referring to a Sapling team **\<dictionary\_entry\_id>**: String (UUID)<br />ID referring to a dictionary entry. HTTP method: `DELETE` ### Response Parameters[​](#response-parameters-2 "Direct link to Response Parameters") Empty, HTTP status code 204. --- # Team Reporting Sapling's API endpoint for exporting team user activity and analytics. See [Team Management](https://sapling.ai/docs/api/team-user-management.md) for getting the IDs for a particular team. ## Organization Teams POST[​](#organization-teams-post "Direct link to Organization Teams POST") ### Request Parameters[​](#request-parameters "Direct link to Request Parameters") Note that **key** and **return\_names** should be part of the request body, while **organization\_id** should be part of the request URL. `https://api.sapling.ai/api/v1/reporting/organization/<organization_id>/teams` **key**: String<br />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. **return\_names**: Optional Boolean<br />Default is false. If true, result have extra field containing team names along with the ids. **\<organization\_id>**: String (UUID)<br />ID of a Sapling organization. API key holder must have manager or admin permissions for the organization. ### Response Parameters[​](#response-parameters "Direct link to Response Parameters") JSON array of team IDs of teams under the organization if `return_names` is `false`, else JSON array of `{"id": "team_id", "name": "team_name"}` entries. ## Organization Usage POST[​](#organization-usage-post "Direct link to Organization Usage POST") API endpoint for retrieving the usage health of teams in an organization. API key holder must have reporting permissions for the organization. ### Request Parameters[​](#request-parameters-1 "Direct link to Request Parameters") ``` import requests key = '<api-key>' url = 'https://api.sapling.ai/api/v1/reporting/organization/<organization_id>/usage' data = { 'key': key, 'start_date': '2026-01-01', 'end_date': '2026-01-08', } try: resp = requests.post(url, json=data) if 200 <= resp.status_code < 300: print(resp) else: print('Error: ', str(resp.status_code), resp.text) except Exception as e: print('Error: ', e) ``` `https://api.sapling.ai/api/v1/reporting/organization/<organization_id>/usage` HTTP method: `POST` **key**: String<br />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. **organization\_id**: String (UUID)<br />ID of a Sapling organization. You can get this information from the URL of the Sapling organization. **start\_date**: String, UTC timezone ISO 8601 date or datetime<br />The maximum time range for reporting is 1 year. Example: `2025-12-01` or `2025-12-01 03:29`. **end\_date**: String, UTC timezone ISO 8601 date or datetime<br />The maximum time range for reporting is 1 year. Example: `2025-12-08` or `2025-12-08 13:59`. If a date without time is specified (like `2025-12-08`), only actions up to previous day (`2025-12-07 23:59:59`) will be included. ### Response Parameters[​](#response-parameters-1 "Direct link to Response Parameters") List of organization usage statistics. This corresponds to `https://sapling.ai/organizations/<organization_id>/usage`. ``` { "assigned_seats": <int>, "active_seats": <int>, "capacity": <int>, "teams": [ ... ] } ``` Each item under `teams` has the following structure: ``` { "team_id": "<team_id>", "team_name": "<team_name>", "assigned_seats": <int>, "active_seats": <int>, "capacity": <int>, "usage_data": { ... } } ``` The `usage_data` field currently has: ``` { "edit_accepts": <int>, "completions": <int>, "rephrases": <int>, "snippets": <int> } ``` where these are the number of times the user has accepted an edit suggestion, used an autocompletion, accepted a rephrased text, or used a snippet, respectively. Please contact us if you need more information included. By default, a user is considered active if they've accepted an edit suggestion, used an autocompletion, or used a snippet. However, you can use the `usage_data` field to customize this. ## User Reporting GET[​](#user-reporting-get "Direct link to User Reporting GET") API key holder must have reporting permissions for the team. ### Request Parameters[​](#request-parameters-2 "Direct link to Request Parameters") ``` import requests key = '<api-key>' url = 'https://api.sapling.ai/api/v1/reporting/team/dashboard_user_data' data = { 'key': key, 'team_ids': ['<team_id1>', '<team_id2>'], 'start_date': '2025-12-01', 'end_date': '2025-12-08', } try: resp = requests.post(url, json=data) if 200 <= resp.status_code < 300: print(resp) else: print('Error: ', str(resp.status_code), resp.text) except Exception as e: print('Error: ', e) ``` `https://api.sapling.ai/api/v1/reporting/team/dashboard_user_data` HTTP method: `POST` **key**: String<br />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. **team\_ids**: Array of Strings (UUID)<br />Array of IDs of Sapling Teams. **start\_date**: String, UTC timezone ISO 8601 date or datetime<br />The maximum time range for reporting is 1 year. Example: `2025-12-01` or `2025-12-01 03:29`. **end\_date**: String, UTC timezone ISO 8601 date or datetime<br />The maximum time range for reporting is 1 year. Example: `2025-12-08` or `2025-12-08 13:59`. If a date without time is specified (like `2025-12-08`), only actions up to previous day (`2025-12-07 23:59:59`) will be included. ### Response Parameters[​](#response-parameters-2 "Direct link to Response Parameters") List of user reporting statistics. If a user is part of multiple teams there will be multiple reporting structures for that user with a different `team_id` field value. User Reporting Structure: ``` { "accepts": <int>, // Number of Sapling edit suggestions accepted "completion_char_counts": <int>, // Character count of autocompletions, for estimating time saved "completion_counts": <int>, // Number of times autocompletion was used "hours_saved": <int>, // Estimate of time saved "id": <str, UUID>, // Opaque user id, used for management "is_admin": <bool>, // This allows for excluding admins from reporting "is_manager": <bool>, // This allows for excluding managers from reporting "login_email": <str, email>, // Login email of user "mean_response_time": <int>, // Mean agent response time in seconds "median_response_time": <int>, // Median agent response time in seconds "rejects": <int>, // Number of Sapling edit suggestions rejected "snippet_char_counts": <int>, // Character count of snippets used, for estimating time saved "snippet_counts": <int>, // Number of snippets used "suggest_helper_selects": <int, optional>, // If Sapling Suggest is enabled for team. "suggest_selects": <int, optional>, // If Sapling Suggest is enabled for team "suggest_views": <int, optional>, // If Sapling Suggest is enabled for team "word_count": <int>, // Total words "team_id": <str, UUID>, } ``` ## Suggest Reporting GET[​](#suggest-reporting-get "Direct link to Suggest Reporting GET") **DEPRECATED** API key holder must have reporting permissions for the team. Team must have Sapling Suggest enabled. ### Request Parameters[​](#request-parameters-3 "Direct link to Request Parameters") ``` import requests key = '<api-key>' url = 'https://api.sapling.ai/api/v1/reporting/team/dashboard_user_suggest_data' data = { 'key': key, 'team_id': '<team_id>', 'start_date': '2025-12-01', 'end_date': '2025-12-08', } try: resp = requests.post(url, json=data) if 200 <= resp.status_code < 300: print(resp) else: print('Error: ', str(resp.status_code), resp.text) except Exception as e: print('Error: ', e) ``` `https://api.sapling.ai/api/v1/reporting/team/dashboard_user_suggest_data` HTTP method: `POST` **key**: String<br />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. **team\_id**: String (UUID)<br />ID of a Sapling team. **start\_date**: String, UTC timezone ISO 8601 date or datetime<br />The maximum time range for reporting is 1 year. Example: `2025-12-01` or `2025-12-01 03:29`. **end\_date**: String, UTC timezone ISO 8601 date or datetime<br />The maximum time range for reporting is 1 year. Example: `2025-12-08` or `2025-12-08 13:59`. If a date without time is specified (like `2025-12-08`), only actions up to previous day (`2025-12-07 23:59:59`) will be included. ### Response Parameters[​](#response-parameters-3 "Direct link to Response Parameters") JSON list of Sapling Suggest reporting stats. Each item in the list has the following structure: ``` { "adjusted_views": <int>, // Responses shown excluding ones within 4 seconds of previous "helper_selects": <int>, "id": <str, UUID>, // Opaque user id, used for management "is_admin": <bool>, // This allows for excluding admins from reporting "is_manager": <bool>, // This allows for excluding managers from reporting "login_email": <str, email>, "mean_response_time": <float>, // Mean agent response time in seconds "median_response_time": <float>, // Median agent response time in seconds "suggest_selects": <int>, // Number of suggested responses selected "suggest_views": <int>, // Number of suggested responses shown } ``` ## Documents Reporting GET[​](#documents-reporting-get "Direct link to Documents Reporting GET") API key holder must have reporting permissions for the team. This endpoint is only applicable to enterprise teams with the Documents dashboard enabled for their team. The endpoint summarizes edit activity per document. ### Request Parameters[​](#request-parameters-4 "Direct link to Request Parameters") `https://api.sapling.ai/api/v1/reporting/team/dashboard_docs_data` HTTP method: `POST` **key**: String<br />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. **team\_id**: String<br />ID of a Sapling team. **start\_date**: String, UTC timezone ISO 8601 date or datetime<br />The maximum time range for reporting is 30 days. Example: `2025-12-01` or `2025-12-01 03:29`. **end\_date**: String, UTC timezone ISO 8601 date or datetime<br />The maximum time range for reporting is 30 days. Example: `2025-12-08` or `2025-12-08 13:59`. If a date without time is specified (like `2025-12-08`), only actions up to previous day (`2025-12-07 23:59:59`) will be included. ### Response Parameters[​](#response-parameters-4 "Direct link to Response Parameters") JSON list of documents and Sapling usage information for each document. Each item in the list has the following structure: ``` { "doc_name": <str>, // Unique string identifying the document "edit_views": <int>, // Number of edits viewed/applied "edit_accepts": <int>, // Number of edits accepted in document "edit_ignores": <int>, // Number of edits ignored in document "doc_timestamp": <int>, // Timestamp that document was created "edit_users": <Array<str, emails[]>>, // Users who performed edit actions } ``` --- # Team Snippets Snippets is a way to save time on frequently used pieces of text. You can read more about snippet usage in Sapling's [support knowledge base](https://saplingai.zendesk.com/hc/en-us/articles/360041373954-Snippets). This is the API mechanism for managing snippets for a team. See [Team Management](https://sapling.ai/docs/api/team-user-management.md) for getting the IDs for a particular team. ## Team Snippet GET[​](#team-snippet-get "Direct link to Team Snippet GET") API key holder must have permissions to read team snippets. ### Request Parameters[​](#request-parameters "Direct link to Request Parameters") ``` import requests key = '<api-key>' team_id = '<TEAM_ID>' url = f'https://api.sapling.ai/api/v1/team/{team_id}/snippets' data = { 'key': key, } try: resp = requests.post(url, json=data) if 200 <= resp.status_code < 300: print('Snippets: ', resp['snippets']) else: print('Error: ', str(resp.status_code), resp.text) except Exception as e: print('Error: ', e) ``` `https://api.sapling.ai/api/v1/team/<team_id>/snippets` HTTP method: `POST` **\<team\_id>**: String (UUID)<br />ID of a Sapling team. **key**: String<br />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. **usage\_stats**: Optional Boolean<br />If `usage_stats` is present, the response will include all-time usage stats of the returned snippets. ### Response Parameters[​](#response-parameters "Direct link to Response Parameters") List of snippets under key `snippets`. ``` { "snippets": [] } ``` Snippet structure: ``` { "id": <str>, // Opaque id, used for management "plain_text": <str, plain text>, // The snippet stripped of rich text formatting "rich_text": <str, markdown/html>, // The snippet "shortcut": <str>, // Expansion shortcut "team_id": <str, UUID>, // Team ID for snippet "title": <str>, // Snippet title "created_at": <str, date>, // e.g. "Wed, 07 Oct 2020 21:15:47 GMT" "updated_at": <str, date>, // e.g. "Wed, 07 Oct 2020 21:15:47 GMT" "stale_at": <str, date>, // e.g. "Wed, 07 Oct 2020 21:15:47 GMT" "expires_at": <str, date>, // e.g. "Wed, 07 Oct 2020 21:15:47 GMT" "user_id": <str, UUID>, // User that created snippet "action_count": <int, optional> // Total usage of snippet, returned if usage_stats was present } ``` --- # User Management ## Teams GET[​](#teams-get "Direct link to Teams GET") Gets all teams the current user is a member of. ### Request Parameters[​](#request-parameters "Direct link to Request Parameters") ``` import requests key = '<api-key>' url = f'https://api.sapling.ai/api/v1/team?key={key}' try: resp = requests.get(url) if 200 <= resp.status_code < 300: resp_json = resp.json() print('Entries: ', resp_json) else: print('Error: ', str(resp.status_code), resp.text) except Exception as e: print('Error: ', e) ``` `https://api.sapling.ai/api/v1/team?key=<key>` HTTP method: `GET` **key**: String<br />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. ### Response Parameters[​](#response-parameters "Direct link to Response Parameters") JSON array of team entries under key `teams`. Team entry structure: ``` { "id": <str, UUID>, // Opaque id, used for team management "name": <str>, // Team name } ``` ## Team POST[​](#team-post "Direct link to Team POST") Create a new team within an organization. This endpoint is available to enterprise plan API key holders who are owners or admins of the organization. ### Request Parameters[​](#request-parameters-1 "Direct link to Request Parameters") `https://api.sapling.ai/api/v1/team` HTTP method: `POST` Requires enterprise plan API key. User must be an owner or admin of the target organization. **key**: String<br />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. **name**: String<br />Name of the team to create. Leading and trailing whitespace is trimmed. The name must: * be non-empty and at most 64 characters * contain at most 2 digits * contain no control characters * contain no links (`://` or `www.`) It must also be unique among the organization's active teams. Deleted teams don't count. **organization\_id**: String (UUID)<br />UUID of the organization where the team will be created. ``` import requests key = '<api-key>' team_name = '<team-name>' org_id = '<organization-id>' url = 'https://api.sapling.ai/api/v1/team' data = { 'key': key, 'name': team_name, 'organization_id': org_id, } try: resp = requests.post(url, json=data) if 200 <= resp.status_code < 300: print('Success') resp_json = resp.json() print('Created team: ', resp_json) else: print('Error: ', str(resp.status_code), resp.text) except Exception as e: print('Error: ', e) ``` ### Response Parameters[​](#response-parameters-1 "Direct link to Response Parameters") #### Success Response (201 Created)[​](#success-response-201-created "Direct link to Success Response (201 Created)") ``` { "id": "team-uuid", "name": "Team Name", "organization_id": "organization-uuid" } ``` #### Error Responses[​](#error-responses "Direct link to Error Responses") **402 Payment Required - No Enterprise Subscription** **400 Bad Request - Missing or Invalid Parameters** The `msg` field says which check failed. For example: * `Team name must be at most 64 characters.` * `Please avoid using numbers in team name.` * `Team name contains invalid characters.` * `Team name cannot contain a link.` * `Team name already exists.` **403 Forbidden - Insufficient Permissions** **401 Unauthorized - Invalid or Missing API Key** ### Example Usage[​](#example-usage "Direct link to Example Usage") #### cURL[​](#curl "Direct link to cURL") ``` curl -X POST https://api.sapling.ai/api/v1/team \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Engineering Team", "organization_id": "YOUR_ORGANIZATION_ID" }' ``` #### Python[​](#python "Direct link to Python") ``` import requests url = "https://api.sapling.ai/api/v1/team" headers = { "Authorization": "Bearer YOUR_API_KEY", "Content-Type": "application/json" } data = { "name": "Engineering Team", "organization_id": "YOUR_ORGANIZATION_ID" } response = requests.post(url, headers=headers, json=data) print(response.json()) ``` #### JavaScript[​](#javascript "Direct link to JavaScript") ``` const response = await fetch('https://api.sapling.ai/api/v1/team', { method: 'POST', headers: { 'Authorization': 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'Engineering Team', organization_id: 'YOUR_ORGANIZATION_ID' }) }); const data = await response.json(); console.log(data); ``` ### Behavior[​](#behavior "Direct link to Behavior") When a team is successfully created: 1. The team inherits settings from the parent organization, such as data persistance. 2. The team receives a subscription matching the organization's subscription: <!-- --> * Same tier (e.g., "enterprise") * Same end date (if applicable) * Same seat capacity (if applicable) 3. The API key holder is automatically added to the team as an admin. ### Notes[​](#notes "Direct link to Notes") * The creating user must be a member of the organization with owner or admin privileges * Only users with enterprise subscriptions can use this endpoint * The endpoint returns the newly created team's UUID, which can be used with other team management endpoints ## Team DELETE[​](#team-delete "Direct link to Team DELETE") Delete a team within an organization. This endpoint is available to enterprise plan API key holders who are owners or admins of the organization. The team must belong to an organization. ### Request Parameters[​](#request-parameters-2 "Direct link to Request Parameters") `https://api.sapling.ai/api/v1/team/<team_id>` HTTP method: `DELETE` Requires enterprise plan API key. User must be an owner or admin of the organization that owns the team. **\<team\_id>**: String (UUID)<br />UUID of the team to delete. **key**: String<br />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. ``` import requests key = '<api-key>' team_id = '<TEAM_ID>' url = f'https://api.sapling.ai/api/v1/team/{team_id}' data = { 'key': key, } try: resp = requests.delete(url, json=data) if 200 <= resp.status_code < 300: print('Success') resp_json = resp.json() print('Team deleted: ', resp_json) else: print('Error: ', str(resp.status_code), resp.text) except Exception as e: print('Error: ', e) ``` ### Response Parameters[​](#response-parameters-2 "Direct link to Response Parameters") #### Success Response (200 OK)[​](#success-response-200-ok "Direct link to Success Response (200 OK)") ``` { "msg": "Team deleted successfully." } ``` #### Error Responses[​](#error-responses-1 "Direct link to Error Responses") **402 Payment Required - No Enterprise Subscription** **400 Bad Request - Invalid Parameters** **404 Not Found - Team Does Not Exist** **403 Forbidden - Insufficient Permissions** **401 Unauthorized - Invalid or Missing API Key** ### Example Usage[​](#example-usage-1 "Direct link to Example Usage") #### cURL[​](#curl-1 "Direct link to cURL") ``` curl -X DELETE https://api.sapling.ai/api/v1/team/YOUR_ORGANIZATION_ID \ -H "Authorization: Bearer YOUR_API_KEY" ``` #### Python[​](#python-1 "Direct link to Python") ``` import requests team_id = "YOUR_ORGANIZATION_ID" url = f"https://api.sapling.ai/api/v1/team/{team_id}" headers = { "Authorization": "Bearer YOUR_API_KEY" } response = requests.delete(url, headers=headers) print(response.json()) ``` #### JavaScript[​](#javascript-1 "Direct link to JavaScript") ``` const teamId = 'YOUR_ORGANIZATION_ID'; const response = await fetch(`https://api.sapling.ai/api/v1/team/${teamId}`, { method: 'DELETE', headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }); const data = await response.json(); console.log(data); ``` ### Behavior[​](#behavior-1 "Direct link to Behavior") When a team is successfully deleted: 1. All team members are deprovisioned from the team: <!-- --> * Team users are marked as deleted * User invites to the team are canceled 2. Subscriptions for the team are ended: ### Notes[​](#notes-1 "Direct link to Notes") * Only teams that belong to an organization can be deleted via this API endpoint * All team members will lose access to the team immediately * The deletion user must be an organization owner or admin, not just a team admin * Only users with enterprise subscriptions can use this endpoint * The operation cannot be undone via the API - contact support if you need to restore a deleted team ## Team Users / Team Invites GET[​](#team-users--team-invites-get "Direct link to Team Users / Team Invites GET") This endpoint is used to get the users and pending users for a team. Pending users are users who have an invite to the team but have not yet joined. API key holder must be an admin for the team. ### Request Parameters[​](#request-parameters-3 "Direct link to Request Parameters") ``` import requests key = '<api-key>' team_id = '<TEAM_ID>' url = f'https://api.sapling.ai/api/v1/team/{team_id}/team_user?key={key}' try: resp = requests.get(url) if 200 <= resp.status_code < 300: print('Success') else: print('Error: ', str(resp.status_code), resp.text) except Exception as e: print('Error: ', e) ``` `https://api.sapling.ai/api/v1/team/<team_id>/team_user?key=<key>` HTTP method: `GET` API key holder must be an admin for the team. **\<key>**: String<br />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. ### Response Parameters[​](#response-parameters-3 "Direct link to Response Parameters") ``` { "users": <Team User>[], // Array of Team Users "pending_users": <Pending User>[], // Array of Pending Users / Invites } ``` #### Team User Structure:[​](#team-user-structure "Direct link to Team User Structure:") ``` { "id": <str, UUID>, // Opaque ID, used for management; each user has a unique id per team "login_email": <str, email>, "is_admin": <bool>, "is_manager": <bool>, "edit_permission": <bool>, // If user has edit permissions "inspect_permission": <bool>, // If user has inspect permissions "created_at": <str, date>, // e.g. "Wed, 07 Oct 2020 21:15:47 GMT" "updated_at": <str, date>, // e.g. "Wed, 07 Oct 2020 21:15:47 GMT" } ``` #### Pending User / Invite Structure:[​](#pending-user--invite-structure "Direct link to Pending User / Invite Structure:") Pending Users have not joined the team yet. Once a user has registered an account and accepted the team invite, they will appear as a Team User. ``` { "email": <str, email>, // Email that was invited "team_id": <str, UUID>, // ID of team that user was invited to "registered": <bool>, // If the email has registered for a Sapling account "confirmed": <bool>, // If the Sapling account created has confirmed their email "permissions": <str>, // Team permissions to be granted when user joins team } ``` ## Team User POST[​](#team-user-post "Direct link to Team User POST") API key holder must be an admin for the team. If adding to teams without invites is allowed: * API key holder also must be a superuser. * An invite will still be sent if an account for the email has not been registered. Upon registration and email confirmation, the account will be automatically associated with the team. ### Request Parameters[​](#request-parameters-4 "Direct link to Request Parameters") ``` import requests key = '<api-key>' team_id = '<TEAM_ID>' url = f'https://api.sapling.ai/api/v1/team/{team_id}/team_user' data = { 'key': key, 'email': '<user_email>', } try: resp = requests.post(url, json=data) if 200 <= resp.status_code < 300: print('Success') else: print('Error: ', str(resp.status_code), resp.text) except Exception as e: print('Error: ', e) ``` `https://api.sapling.ai/api/v1/team/<team_id>/team_user` HTTP method: `POST` **\<team\_id>**: String (UUID)<br />ID of a Sapling team. **key**: String<br />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. **email**: String<br />Email of user to add to team; alternatively, set `emails` for multiple emails **emails**: String<br />List of emails of users to add to team; all will have the same permissions. Alternatively, set `email` for a single email. **NOTE** For now, please use `/team_user2` instead of `/team_user` in the API request URL to use the `emails` argument. This will be resolved in a future release. **is\_admin**: Optional Boolean<br />If the added user will be an admin for the team. Only available if adding to teams without invites is allowed. If the email provided does not have a user account (and an invite is sent), this argument is ignored. **is\_manager**: Optional Boolean<br />If the added user will be a manager for the team. Only available if adding to teams without invites is allowed. If the email provided does not have a user account (and an invite is sent), this argument is ignored. **edit\_permission**: Optional Boolean<br />If the added user will have edit permissions (dictionary, custom errors, snippets) for the team. Only available if adding to teams without invites is allowed. If the email provided does not have a user account (and an invite is sent), this argument is ignored. **inspect\_permission**: Optional Boolean<br />If the added user will have inspect permissions (can view reporting) for the team. Only available if adding to teams without invites is allowed. If the email provided does not have a user account (and an invite is sent), this argument is ignored. ### Response Parameters[​](#response-parameters-4 "Direct link to Response Parameters") Dictionary of the form: ``` { "added": [...], "invited": [...], "errors": [...], "already_exists": [...] } ``` where `added` is a list of `{'email': email, 'team_user': team_user_entry}`, `invited` is a list of `{'email': email}`, `errors` is a list of `{'email', email, 'reason': error_reason}`, and `already_exists` is a list of `{'email': email}`. Team User entry structure: ``` { "id": <str, UUID>, // Opaque ID, used for management; each user has a unique id per team "login_email": <str>, "is_admin": <bool> "is_manager": <bool> "edit_permission": <bool>, // If user has edit permissions "inspect_permission": <bool>, // If user has inspect permissions "created_at": <str, date> // e.g. "Wed, 07 Oct 2020 21:15:47 GMT" "updated_at": <str, date> // e.g. "Wed, 07 Oct 2020 21:15:47 GMT" } ``` ### Invite limits[​](#invite-limits "Direct link to Invite limits") The limit counts invite emails: people who don't have a confirmed Sapling account yet, or who can't be added directly. Each inviter can send at most **25 invite emails per day** from a team that doesn't have an enterprise subscription. Some invites are exempt: * invites from teams on an enterprise subscription * invites from inviters who hold their own active Pro, API or Enterprise subscription (a Pro Trial doesn't count, and neither does a subscription inherited through someone else's team or organization) Some requests don't count toward the limit: * invites that were turned away * invites to someone who is already on the team * re-inviting the same address within 5 minutes, which returns the existing invite without sending another email The window resets 24 hours after the first counted invite. Once an inviter reaches the limit: * `/team_user` returns `400` with `{"msg": "Daily invite limit reached for <email>. Contact support@sapling.ai to invite more people."}`. * `/team_user2` still returns `200`. Each email over the limit is listed under `errors` with that message as its `reason`. Users added directly are unaffected. ## Team User DELETE[​](#team-user-delete "Direct link to Team User DELETE") API key holder must be an admin for the team. This also deletes outstanding invites for the team-email pair, if any exist, but only if the user is already part of the team (otherwise, there is no `team_user_id`). ### Request Parameters[​](#request-parameters-5 "Direct link to Request Parameters") ``` import requests key = '<api-key>' team_user_id = '<TEAM_USER_ID>' url = f'https://api.sapling.ai/api/v1/team_user/{team_user_id}' data = { 'key': key, } try: resp = requests.delete(url, json=data) if 200 <= resp.status_code < 300: print('Success') else: print('Error: ', str(resp.status_code), resp.text) except Exception as e: print('Error: ', e) ``` `https://api.sapling.ai/api/v1/team_user/<team_user_id>` HTTP method: `DELETE` **\<team\_user\_id>**: String (UUID)<br />ID of a Sapling team user. **key**: String<br />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. ### Response Parameters[​](#response-parameters-5 "Direct link to Response Parameters") Empty, HTTP status code 204. ## Team Users Bulk DELETE[​](#team-users-bulk-delete "Direct link to Team Users Bulk DELETE") API key holder must be an admin for the team. This endpoint allows for bulk deletion of users from a team by providing a list of emails. It will attempt to delete each user specified and report which users were successfully deleted and which were not found. Users cannot delete themselves using this endpoint. ### Request Parameters[​](#request-parameters-6 "Direct link to Request Parameters") ``` import requests key = '<api-key>' team_id = '<TEAM_ID>' emails_to_delete = ['user1@example.com', 'user2@example.com'] url = f'https://api.sapling.ai/api/v1/team/{team_id}/team_users/bulk_delete' data = { 'key': key, 'emails': emails_to_delete, } try: resp = requests.delete(url, json=data) if 200 <= resp.status_code < 300: resp_json = resp.json() print('Success:', resp_json) else: print('Error:', str(resp.status_code), resp.text) except Exception as e: print('Error:', e) ``` `https://api.sapling.ai/api/v1/team/<team_id>/team_users/bulk_delete` HTTP method: `DELETE` **\<team\_id>**: String (UUID)<br />ID of the Sapling team from which users will be deleted. **key**: String<br />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. (must belong to a team admin). **emails**: List of Strings<br />A list of email addresses for the users to be deleted from the team. ### Response Parameters[​](#response-parameters-6 "Direct link to Response Parameters") JSON dictionary containing the results of the bulk delete operation. Success status code: `200` ``` { "deleted_users": [ {"id": "<UUID>", "email": "<user1@example.com>"}, // ... other deleted users ], "not_found_users": [ "<user_not_in_team@example.com>" // ... other users not found ], "warning": "" // Reserved for future use } ``` **deleted\_users**: List<br />List of dictionaries, each containing the `id` (Team User ID) and `email` of a user successfully deleted from the team. **not\_found\_users**: List<br />List of emails that were provided in the request but did not correspond to any user in the specified team. Error status codes: * `400`: Bad Request (e.g., missing/invalid `emails` list, invalid `team_id`, attempt to delete self). * `401`/`403`: Unauthorized (API key holder is not an admin for the team). ## Team User PATCH[​](#team-user-patch "Direct link to Team User PATCH") API key holder must be an admin for the team. ### Request Parameters[​](#request-parameters-7 "Direct link to Request Parameters") ``` import requests key = '<api-key>' team_user_id = '<TEAM_USER_ID>' url = f'https://api.sapling.ai/api/v1/team_user/{team_user_id}' data = { 'key': key, 'is_admin': True, } try: resp = requests.patch(url, json=data) if 200 <= resp.status_code < 300: print('Success') else: print('Error: ', str(resp.status_code), resp.text) except Exception as e: print('Error: ', e) ``` `https://api.sapling.ai/api/v1/team_user/<team_user_id>` HTTP method: `PATCH` **\<team\_user\_id>**: String (UUID)<br />ID of a Sapling team user. **key**: String<br />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. **is\_admin**: Optional Boolean<br />If the added user will be an admin for the team. **is\_manager**: Optional Boolean<br />If the added user will be a manager for the team. **edit\_permission**: Optional Boolean<br />If the added user will have edit permissions (dictionary, custom errors, snippets) for the team. **inspect\_permission**: Optional Boolean<br />If the added user will have inspect permissions (can view reporting) for the team. ### Response Parameters[​](#response-parameters-7 "Direct link to Response Parameters") Empty, HTTP status code 204. ## Team User Invite DELETE[​](#team-user-invite-delete "Direct link to Team User Invite DELETE") API key holder must be an admin for the team. This deletes outstanding invites for the team-email pair, if any exist, even if the user has not joined the team. ### Request Parameters[​](#request-parameters-8 "Direct link to Request Parameters") ``` import requests key = '<api-key>' email = '<EMAIL>' url = f'https://api.sapling.ai/api/v1/team_user_invite/{team_id}' data = { 'key': key, 'email': email, } try: resp = requests.delete(url, json=data) if 200 <= resp.status_code < 300: print('Success') else: print('Error: ', str(resp.status_code), resp.text) except Exception as e: print('Error: ', e) ``` `https://api.sapling.ai/api/v1/team_user_invite/<team_id>` HTTP method: `DELETE` **\<email>**: String<br />Email of invited user. **key**: String<br />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. ### Response Parameters[​](#response-parameters-8 "Direct link to Response Parameters") Empty, HTTP status code 204. ## Suppressed Emails GET[​](#suppressed-emails-get "Direct link to Suppressed Emails GET") This endpoint is used to get the list of emails that were invited but were unreachable. **You must request access to this endpoint for your organization.** Suppressed emails must also have a domain matching the domain of the requester. ### Request Parameters[​](#request-parameters-9 "Direct link to Request Parameters") ``` import requests key = '<api-key>' url = f'https://api.sapling.ai/api/v1/organization/{organization_id}/suppressed_emails?key={key}' try: resp = requests.get(url) if 200 <= resp.status_code < 300: print('Success') resp_json = resp.json() print('Suppressed Emails: ', resp_json) else: print('Error: ', str(resp.status_code), resp.text) except Exception as e: print('Error: ', e) ``` `https://api.sapling.ai/api/v1/organization/<organization_id>/suppressed_emails?key=<key>` HTTP method: `GET` **organization\_id**: String (UUID)<br />ID of a Sapling organization. **key**: String<br />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. ### Response Parameters[​](#response-parameters-9 "Direct link to Response Parameters") JSON list of suppressed emails. ``` { "suppressed_emails": ["<EMAIL1>", "<EMAIL2>"] } ``` --- # Tone Detection The tone endpoint checks the tone for a given text. This is similar to our [sentiment endpoint](https://sapling.ai/docs/api/sentiment.md), but provides more fine-grained categories. [Try it out](https://sapling.ai/utilities/tone?utm_campaign=apidocs) The endpoint includes 28 tones ([contact us](https://sapling.ai/contact?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs)) if you're looking for other tones). * `admiring` * `amused` * `angry` * `annoyed` * `approving` * `aware` * `confident` * `confused` * `curious` * `eager` * `disappointed` * `disapproving` * `embarrassed` * `excited` * `fearful` * `grateful` * `joyful` * `loving` * `mournful` * `neutral` (most common) * `optimistic` * `relieved` * `remorseful` * `repulsed` * `sad` * `worried` * `surprised` * `sympathetic` You can find definitions of these tones below. The overall tone is returned as well as the tone for each sentence for a more fine-grained breakdown. ## Tone POST[​](#tone-post "Direct link to Tone POST") ### Sample Code[​](#sample-code "Direct link to Sample Code") * cURL * JavaScript * Python ``` curl -X POST https://api.sapling.ai/api/v1/tone \ -H "Content-Type: application/json" \ -d '{"key":"<api-key>", "text":"Really stoked about this! Will it be ready by next week?"}' ``` ``` import axios from 'axios'; async function run(text) { try { const response = await axios.post( 'https://api.sapling.ai/api/v1/tone', { key: '<api-key>', text, }, ); const {status, data} = response; console.log({status}); console.log(JSON.stringify(data, null, 4)); } catch (err) { const { msg } = err.response.data; console.log({err: msg}); } } run('Really stoked about this! Will it be ready by next week?'); ``` ``` import requests from pprint import pprint response = requests.post( "https://api.sapling.ai/api/v1/tone", json={ "key": "<api-key>", "text": "Really stoked about this! Will it be ready by next week?" } ) if 200 <= response.status_code < 300: pprint(response.json()) else: print('Error: ', response.status_code, response.text) ``` ### Sample Response[​](#sample-response "Direct link to Sample Response") ``` { "overall": [ [ 0.9247264862060547, "curious", "🤓" ], [ 0.04547286033630371, "excited", "😀" ], [ 0.005686695221811533, "confused", "😕" ] ], "results": [ [ [ 0.9936646223068237, "excited", "😀" ], [ 0.001380942645482719, "admiring", "😲" ], [ 0.0009780488908290863, "joyful", "😂" ] ], [ [ 0.5469484329223633, "confused", "😕" ], [ 0.39931708574295044, "curious", "🤓" ], [ 0.040689751505851746, "neutral", "😐" ] ] ], "sents": [ "Really stoked about this!", "Will it be ready by next week?" ] } ``` ### Request Parameters[​](#request-parameters "Direct link to Request Parameters") `https://api.sapling.ai/api/v1/tone` HTTP method: `POST` The Tone API POST endpoint takes JSON parameters documented below: **key**: String<br />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<br />Text to analyze the tone for. ### Response Parameters[​](#response-parameters "Direct link to Response Parameters") The tone endpoint contains three fields: 1. `sents`: The sentences the text contains. 2. `overall`: A list of tuples of the form `(probability, tone, emoji)`. 3. `results`: For each sentence, a list item containing a list of tuples of the form `(probability, tone, emoji)`. Example output: ``` { "overall": [ [ 0.9893065094947815, "neutral", "😐" ], [ 0.0055707162246108055, "curious", "🤓" ], [ 0.0030300025828182697, "confused", "😕" ] ], "results": [ [ [ 0.9893065094947815, "neutral", "😐" ], [ 0.0055707162246108055, "curious", "🤓" ], [ 0.0030300025828182697, "confused", "😕" ] ] ], "sents": [ "Hi, how are you doing?" ] } ``` ## Tone Descriptions and Examples[​](#tone-descriptions-and-examples "Direct link to Tone Descriptions and Examples") To clarify the differences between the different tones, we provide brief definitions and example(s) of each. * **admiring**: With respect and reverence, often used when expressing regard or affection for someone or something. `You're amazing.` * **amused**: Light-hearted and typically signaling laughter or enjoyment of a situation. `Haha, go for it.` * **angry**: Expresses strong displeasure or rage, often characterized by sharp, harsh or loud expressions. `This is stupid.` * **annoyed**: Conveys irritation or frustration, less intense than anger but often tinged with impatience. `You're wasting my time.` * **approving**: Suggests agreement or validation, usually conveying acceptance and positivity towards subject. `That is a fitting match.` * **aware**: Indicates an understanding or consciousness about the subject. * **confident**: Exudes certainty and assurance, often assertive and conveying belief. `We're proud of this work.` * **confused**: Conveys uncertainty or bewilderment, often with a lack of understanding or clarity about a situation. `I have no idea what's going on.` * **curious**: Questioning and often signaling an eagerness to learn or know more about something. `Can you please tell me more?` * **eager**: With enthusiasm and keenness, often characterized by anticipation or readiness to engage. * **disappointed**: Expresses a sense of letdown or dissatisfaction when expectations are not met. `This is not as good as I expected.` * **disapproving**: Conveys a negative judgment, criticism, or dissatisfaction towards a subject or action. `This is not as good as it should be.` * **embarrassed**: Shows feelings of self-consciousness, shame, or awkwardness, often after a mistake or awkward situation. `I felt so self-conscious and awkward.` * **excited**: Full of enthusiasm and anticipation, often showing high energy and eagerness about an event or idea. `Looking forward to it!` * **fearful**: Conveys apprehension, anxiety, or dread, often used when there's a perceived threat or danger. `I'm scared to death.` * **grateful**: Expresses appreciation or thankfulness, often for someone's actions or presence. `Thank you so much.` * **joyful**: Exudes happiness, delight, or pleasure, often full of positive energy and cheerfulness. `What a beautiful day to be alive!` * **loving**: Expresses affection, warmth, and tenderness, often used when expressing feelings of affection and care. * **mournful**: Reveals sorrow or grief, often used when lamenting a loss or a tragic event. `I'm sorry for your loss.` * **neutral**: Maintains an objective or impartial attitude; devoid of strong emotions or bias. `We'll be there in 20 minutes.` * **optimistic**: Conveys hopefulness and confidence about the outcome of something. `The future is full of promise.` * **relieved**: Conveys a sense of release from stress or worry, often following a resolution or good news. `That makes me feel so much better.` * **remorseful**: Expresses regret or guilt over a past action or event, often accompanied by a desire to make amends. `I'm sorry about that.` * **repulsed**: Conveys a strong aversion or disgust towards a subject or situation. `Yuck, get it away!` * **sad**: Expresses unhappiness, sorrow, or grief, generally less intense than mournful but still conveying a negative emotional state. `I feel so miserable.` * **worried**: Conveys unease or concern, often anticipating potential problems or negative outcomes. * **surprised**: Expresses shock or astonishment, usually as a result of an unexpected event or piece of information. `Wow, that's so unexpected!` * **sympathetic**: Shows understanding and compassion towards someone else's situation or feelings. `I can only imagine what you're going through.`   --- # Transcriptions As part of postprocessing, Sapling offers the ability to process speech transcriptions. Speech-to-text (STT), also known as automatic speech recognition (ASR) or speech transcription, has become highly accurate and a convenient way to convert the spoken word to written notes. It can be used in hands-free settings or as a faster alternative to typing. However, transcription systems often introduce artifacts and other undesirable outputs in the resulting transcription. A few examples: * Disfluencies such as `uhh` or `umm`. * Dictation commands such as `new line`, `quote`, `period`, or `next paragraph`. * Blocks of text without structured formatting. For these and other transcription issues, Sapling offers postprocessing configurations that can greatly reduce the amount of manual editing required. [Contact us](https://sapling.ai/contact?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs) to learn more. --- # Translation API The translate endpoint translates text into a **target language you name** — as an ISO 639 code (`"fr"`, `"zh-TW"`) or an English language name (`"French"`) — using a large language model. Line breaks, markup and placeholders (HTML tags, `{braces}`, URLs, email addresses, code) are preserved in place, so translated content drops back into your templates and documents unchanged. The response reports the language the text was **actually written in** (detected from the text, never assumed from your hint) alongside the translation, and an optional `formality` parameter picks a formal or informal register where the target language distinguishes them (vous/tu, Sie/du, usted/tú). Around 170 languages are supported as source and target, plus region variants as targets: `zh-CN` / `zh-TW` / `zh-HK`, `pt-BR` / `pt-PT`, `en-GB` / `en-US`, `fr-CA` and `es-419`. To only identify a language, see the [language detection](https://sapling.ai/docs/api/langdetect.md) endpoint. ## Sample Code[​](#sample-code "Direct link to Sample Code") * cURL * JavaScript * Python * Python (Sapling client) ``` curl -X POST https://api.sapling.ai/api/v1/translate \ -H "Content-Type: application/json" \ -d '{"key":"<api-key>", "text":"Hello world.\nSee you at 5pm — bring the report.", "target_lang": "fr"}' ``` ``` import axios from 'axios'; async function run(text) { try { const response = await axios.post( 'https://api.sapling.ai/api/v1/translate', { key: '<api-key>', text, target_lang: 'fr', formality: 'formal', }, ); 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('Hello world.\nSee you at 5pm — bring the report.'); ``` ``` import requests from pprint import pprint response = requests.post( "https://api.sapling.ai/api/v1/translate", json={ "key": "<api-key>", "text": "Hello world.\nSee you at 5pm — bring the report.", "target_lang": "fr", "formality": "formal", } ) 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) result = client.translate( 'Hello world.\nSee you at 5pm — bring the report.', target_lang='fr', formality='formal', ) print(result['translation']) print(result['source_lang'], result['source_lang_name']) # Region-variant target, named in English: result = client.translate('The meeting is tomorrow.', target_lang='Traditional Chinese') print(result['target_lang']) # zh-TW ``` ## Sample Response[​](#sample-response "Direct link to Sample Response") ``` { "translation": "Bonjour le monde.\nÀ 17h — apportez le rapport.", "source_lang": "en", "source_lang_name": "English", "target_lang": "fr", "target_lang_name": "French" } ``` ## Batch Requests[​](#batch-requests "Direct link to Batch Requests") To translate many short, independent strings in one request, such as UI labels, product titles or a queue of support replies, send `texts` (a list of 1–10 strings) instead of `text`. * The same `target_lang`, `source_lang` and `formality` apply to every item. * The combined length of all items may be up to 5,000 characters, the same cap as a single `text`. * Provide exactly one of `text` and `texts`. ``` curl -X POST https://api.sapling.ai/api/v1/translate \ -H "Content-Type: application/json" \ -d '{"key":"<api-key>", "texts": ["Add to cart", "Your order has shipped."], "target_lang": "de", "formality": "formal"}' ``` The batch response is `{"results": [...]}` with one entry per input, in input order. Each entry has exactly the single-response shape above, including its own detected `source_lang`, so a batch may mix source languages: ``` { "results": [ { "translation": "In den Warenkorb", "source_lang": "en", "source_lang_name": "English", "target_lang": "de", "target_lang_name": "German" }, { "translation": "Ihre Bestellung wurde versandt.", "source_lang": "en", "source_lang_name": "English", "target_lang": "de", "target_lang_name": "German" } ] } ``` **Context.** Each item is translated independently, with no shared context between items. Send related sentences of one document as a single `text` so the model can use the surrounding context. **Billing and caching.** Items are billed individually, so a batch of N costs the same as N single requests. Repeated identical items in one batch are translated and billed once, though each repeat still counts toward a free trial's daily character cap. Each item is cached the same way as a single request, so batches and single calls share the cache. **Failures.** An empty or whitespace-only item returns a `400` naming its index. If any item fails to translate, 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. The [Sapling JavaScript client](https://github.com/saplingai/sapling-js)'s `translate()` accepts an array of strings for the batch form. ## Request Parameters[​](#request-parameters "Direct link to Request Parameters") **POST** to `https://api.sapling.ai/api/v1/translate` **key**: String<br />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<br />The text to translate, up to 5,000 characters. Unlike Sapling's analysis endpoints, the text is **not** HTML-stripped or whitespace-normalized — the translation is the product, so markup and line structure are sent to the model and preserved in place. Text that is empty or whitespace-only returns a `400`. The text is treated as untrusted data: instructions inside it are translated, not followed. Provide exactly one of `text` and `texts`. **texts**: List\[String]<br />Batch form (see [Batch Requests](#batch-requests)): 1–10 texts to translate in one request. Each is treated exactly like `text` above, and their combined length may be up to 5,000 characters. The response becomes `{"results": [...]}`, one entry per item in input order. **target\_lang**: String<br />The language to translate into: an ISO 639 code (`"fr"`, `"de"`, `"ja"`; ISO 639-2/3 codes such as `"ceb"` for languages without a two-letter code) or an English language name (`"French"`). Case and `_`/`-` separators are tolerated (`"pt_BR"` works). Region variants supported as targets: `zh-CN` (Simplified Chinese), `zh-TW` (Traditional Chinese), `zh-HK` (Traditional Chinese, Hong Kong), `pt-BR` / `pt-PT`, `en-GB` / `en-US`, `fr-CA` and `es-419` (Latin American Spanish). An unrecognized language returns a `400` naming the accepted forms. **source\_lang**: String, optional<br />A hint for the language the text is written in, same forms as `target_lang`. It steers ambiguous, very short inputs but never overrides the text: the response's `source_lang` is what was detected. **formality**: String, optional<br />`"formal"` (polite address forms: vous / Sie / usted, formal honorifics) or `"informal"` (familiar forms: tu / du / tú). Omit for the model's default register. Other values return a `400`. ## Response Parameters[​](#response-parameters "Direct link to Response Parameters") **translation**: String<br />The complete translated text, with line breaks, markup and placeholders preserved in place. Content already in the target language is kept as written. **source\_lang**: String<br />Detected source language code, normalized to the same vocabulary the request accepts. `"und"` (undetermined) in the rare case the detection cannot be mapped — the translation itself is still returned. **source\_lang\_name**: String | null<br />English name of the detected source language; `null` when `source_lang` is `"und"`. **target\_lang** / **target\_lang\_name**: String<br />The normalized target: `{"target_lang": "fr", "target_lang_name": "French"}` whether you sent `"fr"`, `"FR"` or `"French"`. Match your application logic on these normalized values. ## Errors[​](#errors "Direct link to Errors") | Status | Meaning | | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `400` | Validation error — missing/oversized/empty `text` (or an empty `texts` item, named by index, or a combined `texts` length over 5,000 characters), both or neither of `text`/`texts`, an unrecognized `target_lang`/`source_lang` (e.g. `{"msg": "Invalid target_lang: Unknown language: \"Klingon\". Send an ISO 639 code (\"fr\", \"zh-TW\") or an English language name (\"French\")."}`) or a bad `formality`. The body is `{"msg": "..."}`. | | `401` / `403` | Missing or invalid API key. | | `429` | Rate limit exceeded or key over capacity. | | `502` | `{"msg": "Unexpected error translating text."}` — the translation model failed; safe to retry. Not billed, and the failed text is not cached. In a batch, any items that did translate are still cached, so a retry only re-runs the failures. | ## Tips[​](#tips "Direct link to Tips") * **Send as much context as fits, not fragments.** The model uses surrounding sentences to resolve pronouns, terminology and register, so translating whole paragraphs beats sentence-by-sentence. Stay within the 5,000-character `text` limit — for longer documents, split on paragraph or section boundaries and translate each chunk, rather than sending isolated sentences. * **Name the variant when it matters.** `zh` lets the model pick a script; `zh-TW` pins Traditional Chinese. Likewise `pt-BR` vs `pt-PT` and `en-GB` vs `en-US` for spelling conventions. * **Set `formality` for customer-facing content** in languages with a T–V distinction (French, German, Spanish, Japanese honorifics) — the default register is the model's judgment call. * **Mixed-language input is fine**: the detected `source_lang` is the dominant language, and content already in the target language is left as written. * Results are cached on Sapling's side for about three days, keyed on the text, target, source hint and formality — repeat requests are fast and not double-billed within the [usage dedup window](https://sapling.ai/docs/api/pricing).   --- # advanced\_edit Options for advanced edits. Option keys are `String`, and option values are `Boolean`. * `advanced_edits`: Group setting, covering all the options below * `adverbs` * `simplifications` * `hard_to_read` * `qualifiers` * `voice` * `dei`: Group setting, covering all DEI options below * `gender`: Group setting, covering `gender_pronoun`, `gender_noun`, and `gender_id` * `gender_pronoun` * `gender_noun` * `gender_id` * `sensitivity` * `disability` * `age` * `race` * `social_class` * `violence` A group setting marked `false` will disable all categories it covers. --- # HTML\_custom\_UI ``` <html> <head> <style type="text/css"> body, textarea { background-color: black; color: white; } </style> </head> <body> <textarea sapling-ignore="true" id="text-element" rows="10" cols="40" style="padding:20px"> Lets get started. </textarea> </body> <script src="https://sapling.ai/static/js/sapling-sdk-v1.1.1.min.js"></script> <script type="text/javascript"> var customCSS = ` #sapling-edit-controls #edit-controls-buttons { border: 0.5px solid #333; } #sapling-edit-controls #accept-button, #sapling-edit-controls #ignore-button { background-color: #111; } #sapling-edit-controls .icon-sapling-accept, #sapling-edit-controls #accept-text { color: green; } #sapling-edit-controls #accept-button:hover, #sapling-edit-controls #ignore-button:hover{ background-color: #222; } #sapling-edit-controls .icon-sapling-reject, #sapling-edit-controls #ignore-text { color: #565656; } #sapling-edit-controls #ignore-row { border-top: 0.5px solid #333; } `; Sapling.init({ key: '<api-key>', mode: 'dev', appearance: { customCSS } }); const textarea = document.getElementById('text-element'); Sapling.checkOnce(textarea); </script> </html> ``` --- # HTML\_dev ``` <html> <head> <script src="https://sapling.ai/static/js/sapling-sdk-v1.1.1.min.js"></script> </head> <body> <div contenteditable="true" id="check-space" sapling-ignore="true">Lets get started!</div> <script type="text/javascript"> Sapling.init({ key: '<api-key>', mode: 'dev', }); const contentEditable = document.getElementById('check-space'); Sapling.observe(contentEditable); </script> </body> </html> ``` --- # HTML ``` <html> <head> <script src="https://sapling.ai/static/js/sapling-sdk-v1.1.1.min.js"></script> </head> <body> <div contenteditable="true" id="editor" sapling-ignore="true">Lets get started!</div> <script type="text/javascript"> Sapling.init({ endpointHostname: 'http://localhost:3000', saplingPathPrefix: '/sapling', }); const contentEditable = document.getElementById('editor'); Sapling.observe(contentEditable); </script> </body> </html> ``` --- # python\_proxy ``` import requests from flask import Flask, jsonify, request from flask_cors import CORS app = Flask(__name__) CORS(app, origins='*') SAPLING_API_HOSTNAME = 'https://api.sapling.ai' API_KEY = '<api-key>' @app.route('/sapling/<path:path>', methods=['POST']) def sapling_proxy(path): request_url = f'{SAPLING_API_HOSTNAME}/{path}' args = request.get_json() args['key'] = API_KEY result = requests.post(request_url, json=args) result = result.json() return jsonify(result), 200 ``` --- # dev\_api\_key\_tip Developer Key A rate-limited API developer key can be provisioned from the [Sapling API dashboard](https://sapling.ai/api_settings?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs). Developer keys allow for processing of 50,000 characters every 24 hours. [Subscribe](https://sapling.ai/api-checkout) for production access and [usage-based pricing](https://sapling.ai/docs/api/pricing). --- # languages Specifies language for grammar and spell checking. Defaults to English, or the language specified in the dashboard. Supported languages: * `auto`: Autodetect language * `de`: German (Deutsch) * `el`: Greek (Ελληνικά) * `en`: English (US/UK/CA/AU) * `es`: Spanish (Español) * `fr`: French (Français) (`fr-fr` and `fr-ca` coming soon) * `it`: Italian (Italiano) * `jp`: Japanese (日本語) * `ko`: Korean (한국어) * `nl`: Dutch (Nederlands) * `pl`: Polish (Polski) * `pt`: Portuguese (Português) (`pt-pt` and `pt-br` coming soon) * `sv`: Swedish (Svenska) * `tl`: Tagalog * `zh`: Chinese (中文) --- # varieties This option is currently available for: English, Chinese (中文). Specifies regional English/Chinese spelling options. The setting defaults to the configuration in the Sapling dashboard. Supported varieties: English: * `us-variety`: American English * `gb-variety`: British English * `au-variety`: Australian English * `ca-variety`: Canadian English * `null-variety`: Don’t suggest any changes based on English variety Chinese (中文) * `s-variety`: Simplified Chinese * `t-variety`: Traditional Chinese --- # Custom Models ## Language Variety[​](#language-variety "Direct link to Language Variety") Just as different regions of the world use different languages and different English-speaking regions use different varieties of English, different companies use company and industry-specific language as well. Beyond differing lexicons, different settings and industries have differing syntactic and stylistic preferences as well. One common example is an informal (chat, texting, colloquial) vs. formal (email, memo, business) style. | Informal | Formal | | ------------------ | -------------------------------- | | hey wuts up | Hi, how are you? | | brb | I'll be with you in a moment. | | cya tomorrow | Speak with you tomorrow. | | that photo is bomb | That's a wonderful photo. | | dude, i'm lost | Sorry, could you please clarify? | Example industries that have industry-specific styles: * Academic publishing * Healthcare * Legal * Finance * Speech transcripts These industries usually have writers and scribes that specialize in the conventions and vernaculars of the industry. ## Approaches[​](#approaches "Direct link to Approaches") While a marketing or content manager can sit down and create a style guide that serves as a good starting point, a manually specified guide is unlikely to capture the long tail of possible scenarios and language that users will encounter and generate. This is because language is rich and complex, and although rules of thumb can be helpful, they inevitably miss many phenomena. Instead, machine learning (ML) allows systems to **learn from data** the patterns and nuances of particular industries and language varieties. The more data is provided, the better ML systems perform. Sapling uses this approach. ## Prerequisites[​](#prerequisites "Direct link to Prerequisites") While people can acquire a sense of a specific style from perhaps a dozen or so pages, ML systems still require a lot more data. We recommend custom models are trained on at least **200K** sentences worth of data, though the more data, the better (Sapling systems are usually trained with at least a few million sentences or sentence pairs). This ensures that the system has a representative sample of text that will be similar enough to future text it encounters (in ML, we refer to this as generalization / avoiding overfitting). In addition, training large ML models in a reasonable amount of time requires that Sapling uses graphics processing units (GPUs). Each training run requires \~$2000 in compute spend, and the development of a model may require multiple runs to benchmark performance with different settings. ## Evaluation Set[​](#evaluation-set "Direct link to Evaluation Set") Prior to training a model, we first set aside a few thousand sentences in what is referred to as an **evaluation** or **validation set**. We use the validation set to benchmark the performance of the model once it is trained. In the case where we finetune an existing model, we can use the validation set to measure performance before and after finetuning to determine the performance gains. The validation set should meet the following guidelines: 1. It should be similar to / reflect the production data the customer expects. It should also have gone through the same preprocessing pipeline as the production data. 2. It should be at least several hundred (if not more) examples. 3. It should not intersect with the training set (this is called data contamination). Please read our [evaluation guide](https://sapling.ai/docs/api/evaluation.md) for more details. ## Input Format[​](#input-format "Direct link to Input Format") For most applications, we only need to be able to take input data (whether it be in `XML`, `JSON`, `HTML`, or `TXT` format) and extract the plain text. The text should not include formatting characters or markup (unless we are specifically training a formatting model). Because of challenges in [text extraction](https://stackoverflow.com/questions/51035220/why-is-it-so-hard-to-convert-pdf-to-plain-text), we usually cannot work with PDF files. However, once the text is in one of the plain text formats described above, it is usually straightforward to convert it to a format the Sapling team can use to train or finetune a custom model. ## Results[​](#results "Direct link to Results") Once a model is trained, there are two steps to evaluate performance. 1. Evaluate performance on the validation set. If the model does not exhibit significantly better performance, it is highly unlikely to perform better once deployed. The metric that we usually use to evaluate performance with the validation set is [F-score](https://en.wikipedia.org/wiki/F-score). 2. Deploy the model to a test group that works with production data. This is the real test to determine of deploying a custom model has helped. Here the metric is usually any change in the number of suggestions accepted/ignored by users or a more qualitative survey. (Read more about evaluation in [this article](https://sapling.ai/docs/api/evaluation.md).) We've seen significant gains for businesses in healthcare, publishing, and even direct-to-consumer sales. Contact us to learn more. <br /> [Contact Us](https://sapling.ai/custom?utm_campaign=apidocs) ## Frequently Asked Questions[​](#frequently-asked-questions "Direct link to Frequently Asked Questions") **Q: How long does it take to develop a custom model?** A: Typically a few weeks for data collection, then another 1-2 weeks for deployment. Data collection usually takes the most time. **Q: How often should models be updated?** A: We recommend reviewing performance quarterly and updating as needed. **Q: How can users provide feedback?** A: Sapling's SDK provides an accept/ignore button for each suggestion. Sapling can use these actions to iteratively improve the model. --- # Data Transfer, Processing, & Retention ## Activation[​](#activation "Direct link to Activation") Through Sapling's [integrations](https://sapling.ai/integrations?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs) and SDK, text contained in editable page elements is processed and sent to the Sapling backend server. The SDK is programmatically attached to specific text fields on the developer's domains. In contrast, the extension is enabled on all websites. In the **Site Controls** section of the Sapling dashboard, the extension can be configured to run only on specific hostnames. As an example, here is the setting for enabling Sapling **only** on `mail.acme.com`: | Hostname | Enabled? | | --------------- | -------- | | `*` | `false` | | `mail.acme.com` | `true` | Within the webpages of each hostname, Sapling activates on `textarea` (long, plain text field) elements and `contenteditable` (rich text field) elements. If desired, Sapling can also be enabled on `input` (short, plain text field) elements. ## Data Transfer[​](#data-transfer "Direct link to Data Transfer") Once text is sent from text fields our through Sapling's API, the text is sent to the Sapling backend server. The default backend endpoint is Sapling's servers at `https://api.sapling.ai` which are located on Amazon Web Services (AWS) in the `us-west` region. For companies that cannot transfer their data outside of a particular region (for example some companies in the European Union) or companies in industries such as government or finance, Sapling also offers **cloud-premises** and **on-premises** deployments. AWS, Azure, and GCP are supported for cloud-premises deployments. On-premises deployments are supported as long as users can run Docker containers. Data in transit is TLS 1.2 encrypted (HTTPS). Refer to Sapling's [security page](https://sapling.ai/security?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs) for more information. Data sent from the end user to Sapling includes: * Text in textareas and contenteditables where Sapling is activated. * "Accept" and "ignore" events where Sapling's suggestions are accepted or ignored. * Credentials for authentication. Data returned from Sapling's backend to the user includes: * Edits, completions, and suggested responses in JSON format. * Authentication tokens. Contact us for a list of subprocessors. ## Data Processing & Retention[​](#data-processing--retention "Direct link to Data Processing & Retention") Once sent to Sapling's backend, Sapling first **discards** information including numbers/numerical expressions (e.g., phone numbers), email addresses, and URLs. The data is then processed within Sapling's virtual private cloud before suggested edits and completions are sent back to the requester. Data at rest is AES-256 encrypted. Refer to Sapling's [security page](https://sapling.ai/security?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs) for more information. Sapling also offers a **no data retention** option where data is never stored on disk. Otherwise, clients can also specify the period of time for which they would like data to be retained. For the AI detector API, no data retention is also available per request: send `"zero_retention": true` and the text is not cached or stored with usage records. See [AI Detector: Data retention](https://sapling.ai/docs/api/detector.md#data-retention). --- # Support There are several ways to get answers to any questions you have about Sapling. The first place to look is our [Support Center](https://sapling.ai/support?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs). There you'll find detailed documentation on the Sapling product, its features, and various configuration options. You can also join our [Discord](https://discord.gg/p5gDc684TX), start a Stack Overflow thread (and send us the link), or DM us on [Twitter](https://twitter.com/saplingai). If you don't get a response that way, try creating a [ticket](https://saplingai.zendesk.com/hc/en-us/requests/new) or emailing `support@sapling.ai`. ## Frequently Asked Questions[​](#frequently-asked-questions "Direct link to Frequently Asked Questions") #### We've hit the daily or monthly quota limit. Can Sapling provide additional quota for testing?[​](#weve-hit-the-daily-or-monthly-quota-limit-can-sapling-provide-additional-quota-for-testing "Direct link to We've hit the daily or monthly quota limit. Can Sapling provide additional quota for testing?") Sapling considers these requests on a case-by-case basis. If you have a benchmark that you're evaluating different solutions on, typically we can provide a significantly higher quota. However, we do not provide additional quota for free trials if we believe a statistically significant evaluation of Sapling's performance can be performed with the existing quota. Please [Subscribe](https://sapling.ai/api-checkout) to remove quota limits and begin [usage-based pricing](https://sapling.ai/docs/api/pricing). If you're using the SDK, you can use [`checkOnce`](https://sapling.ai/docs/sdk/JavaScript/frontend_documentation.md#saplingcheckonce) or stub the backend endpoint with cached edits to greatly reduce quota usage. #### Why is Sapling not suggesting any edits for my text containing errors?[​](#why-is-sapling-not-suggesting-any-edits-for-my-text-containing-errors "Direct link to Why is Sapling not suggesting any edits for my text containing errors?") A few steps to try: 1. Make sure you're using the `/edits` endpoint and not the `/spellcheck` endpoint if you're considering errors beyond spelling. 2. Review our section on API performance evaluation [here](https://sapling.ai/docs/api/evaluation.md). 3. Make sure your `lang` and `variety` settings are correct (if set). #### How do I apply the outputs of the edits returned by Sapling's API?[​](#how-do-i-apply-the-outputs-of-the-edits-returned-by-saplings-api "Direct link to How do I apply the outputs of the edits returned by Sapling's API?") Please see the tips [here](https://sapling.ai/docs/api/applying-edits.md). #### Why is the API latency higher than normal?[​](#why-is-the-api-latency-higher-than-normal "Direct link to Why is the API latency higher than normal?") The primary way to reduce API latency is by sending smaller chunks of text. Although Sapling accepts chunks of up to 4000 characters, it will take much longer to process such a chunk than one of say only a few hundred characters. Latency will also depend on the region you use Sapling from. After signing in, you can view the latest latency sampled from different regions on the [status page](https://sapling.ai/status?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs). You can also read more about Sapling's status and uptime [here](https://sapling.ai/docs/uptime.md). #### Why am I getting the error `Authentication failed because the remote party has closed the transport stream.`?[​](#why-am-i-getting-the-error-authentication-failed-because-the-remote-party-has-closed-the-transport-stream "Direct link to why-am-i-getting-the-error-authentication-failed-because-the-remote-party-has-closed-the-transport-stream") If you're using .NET, this is likely occurring because you haven't set SecurityProtocol to TLS 1.2 or above. See [here](https://codeshare.co.uk/blog/how-to-fix-the-error-authentication-failed-because-the-remote-party-has-closed-the-transport-stream/) for more information. #### Can I white-label the Sapling SDK for my application?[​](#can-i-white-label-the-sapling-sdk-for-my-application "Direct link to Can I white-label the Sapling SDK for my application?") Yes, developers are allowed to white-label the SDK. By default, the SDK displays a small badge but no Sapling logo or identifying text. For additional questions or custom branding options, please contact <team@sapling.ai>. --- # Other Integrations With developer platforms, there is often a trade-off between full customizability (power) and ease of use (convenience). This is also true for Sapling's developer platform. The most customizable and powerful point of integration is by using the [API](https://sapling.ai/docs/api/api-access.md). Using the API endpoints, you can develop your own frontend to display suggestions using whatever library or framework you prefer. For many, writing your own HTML/CSS/JavaScript plugin is unnecessary. Instead, you can use Sapling's [SDK](https://sapling.ai/docs/sdk/overview.md) to get up and running with just a few lines of JavaScript. While it may be trickier to configure Sapling's UI exactly to your liking, we've already implemented a minimal and — most importantly — functional interface that doesn't require end users to install any additional plugins. note If you'd like to customize Sapling in a way that is not described in this documentation, reach out to us. We're often able to add additional configuration options. However, sometimes it may make sense to just use Sapling's [extensions and add-ins](https://sapling.ai/integrations?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs). We offer browser extensions, Microsoft add-ins, and Google app integrations that cover the majority of platforms. While in many cases the end user has to install these integrations, oftentimes business admin controls can be configured to automatically install Sapling on employee browsers and other applications. Ultimately, we hope that whatever level of developer expertise you have available, Sapling can support your use case. --- # Sapling MCP Server The Sapling MCP Server makes Sapling's language APIs available as tools in Claude Desktop, Claude Code, ChatGPT connectors, and other assistants that speak the [Model Context Protocol](https://modelcontextprotocol.io/). There are two ways to connect: | | Hosted server | Local server | | --------- | -------------------------------------------- | ----------------------------------- | | Address | `https://mcp.sapling.ai/mcp` | runs on your machine via `npx` | | Transport | Streamable HTTP | `stdio` | | Requires | An MCP client that supports remote servers | Node.js 18+ | | Sign-in | Sapling account (OAuth) or an API key header | API key in the client's environment | Use the hosted server unless your client only supports local `stdio` servers, or you need requests to go to an on-premises Sapling deployment. ## Hosted server[​](#hosted-server "Direct link to Hosted server") Add `https://mcp.sapling.ai/mcp` as a remote MCP server in your client. On first connection the client opens a Sapling page in your browser where you sign in and choose which of your API keys the connection should use. Approve it, and the client receives a token — no key is ever pasted into the client. Claude Code: ``` claude mcp add --transport http sapling https://mcp.sapling.ai/mcp ``` Then run `/mcp` in Claude Code and choose **Authenticate** to complete sign-in. In Claude Desktop or claude.ai, add a custom connector with the same URL. ### Connecting with an API key instead[​](#connecting-with-an-api-key-instead "Direct link to Connecting with an API key instead") Clients that let you set request headers can skip the sign-in flow and send a [Sapling API key](https://sapling.ai/docs/api/api-access.md) directly: ``` claude mcp add --transport http sapling https://mcp.sapling.ai/mcp \ --header "Authorization: Bearer YOUR_API_KEY" ``` Requests without a credential are refused with `401`. ### What a connection can do[​](#what-a-connection-can-do "Direct link to What a connection can do") An approved connection can call the Sapling tools listed below using the API key you selected. Usage counts against that key's quota and billing, exactly as your own API calls do. The connection cannot read your documents, team data, or account settings, and it cannot create or change API keys. Access tokens last 24 hours and are renewed automatically by the client for up to 90 days. To disconnect an application, delete or rotate the key it uses in [API settings](https://sapling.ai/api_settings?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs), or remove the server from your MCP client. ## Local server[​](#local-server "Direct link to Local server") The local server runs through Node.js. When you call a Sapling tool, it sends the requested text to the Sapling API and returns the API's structured response to your assistant. ### Before you begin[​](#before-you-begin "Direct link to Before you begin") You need: 1. [Node.js](https://nodejs.org/) 18 or newer, including the `npx` command. 2. A [Sapling API key](https://sapling.ai/docs/api/api-access.md). Trial keys are available after registration. 3. An MCP client that supports local `stdio` servers. Keep the API key in your MCP client's environment configuration. Do not place it in prompts, source code, or a committed configuration file. ### Claude Desktop[​](#claude-desktop "Direct link to Claude Desktop") Add the following entry to your Claude Desktop configuration, replacing `YOUR_API_KEY` with your Sapling API key: ``` { "mcpServers": { "sapling": { "command": "npx", "args": ["-y", "@saplingai/mcp-server"], "env": { "SAPLING_API_KEY": "YOUR_API_KEY" } } } } ``` The configuration file is located at: * macOS: `~/Library/Application Support/Claude/claude_desktop_config.json` * Windows: `%APPDATA%\Claude\claude_desktop_config.json` Restart Claude Desktop after saving the file. ### Claude Code[​](#claude-code "Direct link to Claude Code") Run: ``` claude mcp add sapling --env SAPLING_API_KEY=YOUR_API_KEY \ -- npx -y @saplingai/mcp-server ``` Then run `claude mcp list` to confirm that `sapling` is configured. #### Tool permissions[​](#tool-permissions "Direct link to Tool permissions") Claude Code asks for approval the first time Claude calls each Sapling tool. Choose the "don't ask again" option to approve a tool permanently. To pre-approve tools — or if a tool call is denied instead of prompted, which can happen in auto-accept or non-interactive sessions — run `/permissions` in Claude Code and add an allow rule: * `mcp__sapling` allows every Sapling tool. * `mcp__sapling__sapling_grammar_check` allows one tool. Rule names follow `mcp__<server>__<tool>`, where the server name is the one you passed to `claude mcp add` (`sapling` above). MCP permission rules do not support wildcards; use `mcp__sapling` to allow all Sapling tools. You can also add the rules directly to the `permissions.allow` list in `~/.claude/settings.json` (all projects) or `.claude/settings.json` (one project): ``` { "permissions": { "allow": ["mcp__sapling"] } } ``` ### Other MCP clients[​](#other-mcp-clients "Direct link to Other MCP clients") Use the following local-server settings in any client that accepts a command, arguments, and environment variables: | Setting | Value | | ----------- | ------------------------------ | | Transport | `stdio` | | Command | `npx` | | Arguments | `-y`, `@saplingai/mcp-server` | | Environment | `SAPLING_API_KEY=YOUR_API_KEY` | With this configuration your client starts the local `stdio` process and the process calls `https://api.sapling.ai`. For a remote server, use `https://mcp.sapling.ai/mcp` as described in [Hosted server](#hosted-server). ### Configuration[​](#configuration "Direct link to Configuration") These environment variables configure the local `stdio` server. The hosted server is configured through its sign-in flow or `Authorization` header instead. | Environment variable | Required | Default | Description | | ------------------------ | -------- | ------------------------ | ----------------------------------------------------------------- | | `SAPLING_API_KEY` | Yes | — | Sapling API key used for each request | | `SAPLING_API_BASE_URL` | No | `https://api.sapling.ai` | Alternate Sapling API base URL, such as an on-premises deployment | | `SAPLING_MCP_TIMEOUT_MS` | No | `120000` | Positive request timeout in milliseconds | ## Available tools[​](#available-tools "Direct link to Available tools") Starting with the `@saplingai/mcp-server` release after 0.1.10, the two servers offer the same tools. Two tools are spelled differently on each — the hosted names were published first and cannot change without breaking connected clients. | Tool (hosted server) | Purpose | Optional inputs | On the local server | | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | | `sapling_grammar_check` | Grammar, spelling, punctuation, and style suggestions. Pass `include_error_categories: false` to skip error classification when you only need the corrections (see [Edits API](https://sapling.ai/docs/api/edits-overview.md)) | `session_id`, `lang`, `variety` (`us-variety`, `gb-variety`, `au-variety`, `ca-variety`, `s-variety`, `t-variety`, or `null-variety`), `auto_apply`, `include_error_categories` | same name (also takes `medical` and `advanced_edits`) | | `sapling_spellcheck` | Spelling-focused suggestions | `session_id`, `lang` | `sapling_spell_check` | | `sapling_ai_detect` | AI-generated content detection (see the [AI detector API](https://sapling.ai/docs/api/detector.md)) | `sent_scores`, `lang` | same name (also takes `score_string`) | | `sapling_ai_detect_url` | AI-generated content detection for a public web page or linked document: Sapling fetches the URL, scores its main readable text, and adds a `source` block (final URL, title, content type, characters scored, `truncated`); see [Checking Web Pages](https://sapling.ai/docs/api/detector.md#checking-web-pages) | `sent_scores`, `score_string`, `version`, `lang` | same name (the `@saplingai/mcp-server` release after 0.1.10) | | `sapling_rephrase` | Paraphrase, tone, and tense transformations | `mapping`, `tone_mapping`, `tense_mapping`, `num_results` (1–8) | same name | | `sapling_summarize` | Summarize a longer text, webpage, or email thread into a prose `summary` and a `key_points` list (see the [Summarize API](https://sapling.ai/docs/api/summarize.md)) | `length` (`short`, `medium`, or `long`) | same name (`length` from the `@saplingai/mcp-server` release after 0.1.10) | | `sapling_translate` | Translate text into a target language (ISO 639 code or English name; region variants like `zh-TW`, `pt-BR` supported), preserving line breaks and markup, with the detected source language reported | `target_lang` (required), `source_lang`, `formality` | same name | | `sapling_sentiment` | Analyze sentiment | None | same name | | `sapling_detect_language` | Detect a text's language, with a `reliable` flag, the writing `script` (ISO 15924), runner-up candidates and an optional per-segment breakdown for mixed-language text | `top_k` (1–10), `segments` | `sapling_language_detect` | | `sapling_detect_pii` | Detect PII (emails, phones, SSNs, cards, IPs, IBANs, routing numbers — plus opt-in LLM-backed `person`/`organization`/`location`; on the local server from `@saplingai/mcp-server` 0.1.10) with exact offsets and optionally return a redacted copy | `types`, `redact` | same name | | `sapling_safety` | Content-safety scores across seven categories (toxicity, profanity, harassment, hate speech, self-harm, sexual content, violence) with flagged categories, and optionally the offending passages as spans | `threshold`, `spans` | same name | | `sapling_quality` | Score writing quality 1–5, optionally per sentence and with an LLM rubric (clarity, coherence, correctness, concision, quoted issues with suggested rewrites) | `sentence_scores`, `rubric` | same name | | `sapling_seo` | SEO analysis and metadata: on-page stats, keyword density, top terms, and suggested title tags, meta descriptions, slug and focus keywords | `keywords` (up to 10), `suggestions`, `lang` | same name | | `sapling_classify` | Zero-shot classification into labels you supply (single- or multi-label), with a score per label and a one-sentence rationale | `labels` (required, 2–20), `multi_label`, `threshold`, `context` | same name | | `sapling_extract` | Extract fields you name from unstructured text, with a typed value and the quoted source span for each | `fields` (required, 1–20), `context` | same name | | `sapling_styleguide` | Check text against your own style-guide rules, or a saved rule set, and return each violation with its offsets and a suggested fix (see [Style Guide API](https://sapling.ai/docs/api/styleguide.md)) | `rules` or `ruleset` (exactly one) | same name (@saplingai/mcp-server 0.1.9+) | | `sapling_simplify` | Rewrite text in plain language at a target reading level, keeping the terms you name unchanged (see [Simplify API](https://sapling.ai/docs/api/simplify.md)) | `reading_level`, `preserve_terms`, `lang` | same name (@saplingai/mcp-server 0.1.9+) | | `sapling_ner` | Named-entity recognition (people, organizations, locations and more) with offsets (see [NER API](https://sapling.ai/docs/api/ner.md)) | `types` | same name (@saplingai/mcp-server 0.1.9+) | | `sapling_inclusive` | Flag non-inclusive or biased wording (gendered defaults, ableist idioms, ageist framing...) with offsets, a category, a severity and a neutral replacement (see the [Inclusive Language API](https://sapling.ai/docs/api/inclusive.md)) | `categories` | same name (the `@saplingai/mcp-server` release after 0.1.10) | | `sapling_tone` | Classify emotional tone, overall and per sentence (see the [Tone API](https://sapling.ai/docs/api/tone.md)) | None | same name (the `@saplingai/mcp-server` release after 0.1.10) | | `sapling_readability_statistics` | Grade level, reading time, and text counts (see the [Statistics API](https://sapling.ai/docs/api/statistics.md)) | `lang` | same name (the `@saplingai/mcp-server` release after 0.1.10) | | `sapling_autocomplete` | Predict how a partial sentence continues (see the [Autocomplete API](https://sapling.ai/docs/api/autocomplete.md)) | `session_id` | same name (the `@saplingai/mcp-server` release after 0.1.10) | Every tool requires a non-empty `text` input, except `sapling_ai_detect_url`, which takes `url` (an `http` or `https` address, 2,048 characters maximum), and `sapling_autocomplete`, which takes `query` (200 characters maximum) and needs a key with autocomplete enabled. For endpoint-specific limits and response formats, see the [API documentation](https://sapling.ai/docs/api.md). ### Tool results[​](#tool-results "Direct link to Tool results") A successful tool call returns the API's JSON response as a text block, the same body the matching [API endpoint](https://sapling.ai/docs/api.md) returns. The hosted server also returns that response as [`structuredContent`](https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content) when it is a JSON object. Clients on MCP protocol 2025-06-18 or later can then read scores, offsets and edits as typed fields instead of parsing the text. The local server does the same starting with the next `@saplingai/mcp-server` release after 0.1.10. * The text block is always there, so older clients keep working unchanged. * A response that is a list or a single value is returned as text only. So is a failed call (`isError: true`), whose text describes the error. * No tool declares an `outputSchema`. The API adds response fields over time, and a declared schema would turn each new field into a breaking change. ### Errors and retries[​](#errors-and-retries "Direct link to Errors and retries") A failed call comes back as a tool result with `isError: true`. Its text carries the API's HTTP status and error message, so an agent can read it and decide what to do. Invalid or missing credentials are the exception: the hosted server answers those with an authorization challenge, so your client signs in again. When the block clears by waiting, the error also says how long to wait. That covers a short-term or monthly rate limit, a free-trial daily allowance, and a temporarily overloaded service: * The hosted server ends the text with `Wait at least N seconds before retrying.` * The local server adds a top-level `retry_after` (in seconds) to its JSON error, next to `error`, `status`, and the API's raw `body`. This starts with the next `@saplingai/mcp-server` release after 0.1.10. A `500`, a `502`, or a `400` whose message starts with `Unexpected error` is a temporary server or model failure and has no wait time. Retry it once or twice with a short backoff, as [Rate Limits & Errors](https://sapling.ai/docs/rate_limits.md#retrying-requests) describes. Any other error with no wait time won't succeed by retrying the same call. Fix the account in your [API settings](https://sapling.ai/api_settings?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs), or shorten the input text. ### Prompts and reference resources[​](#prompts-and-reference-resources "Direct link to Prompts and reference resources") The hosted server also publishes ready-made prompts — proofread, adjust tone, check for AI-generated content, summarize, and readability report — which clients surface as slash commands or quick actions, and reference resources under `sapling://reference/` listing the language codes and argument values each tool accepts. ## Try it[​](#try-it "Direct link to Try it") After restarting or reconnecting your MCP client, try: * "Check this sentence for grammar: `This are a test.`" * "Classify this support ticket as billing, technical issue, shipping or other." * "Detect the language of: `Bonjour tout le monde.`" * "Pull the invoice number, total and due date out of this email." * "Rewrite this sentence in a friendlier tone." * "Suggest a title tag and meta description for this article, targeting `grammar checker`." * "Analyze the sentiment of this customer message." * "Redact any PII from this support transcript before I paste it into the ticket." Your assistant should ask for confirmation when appropriate and then call a tool whose name begins with `sapling_`. ## Troubleshooting[​](#troubleshooting "Direct link to Troubleshooting") ### `Missing SAPLING_API_KEY environment variable`[​](#missing-sapling_api_key-environment-variable "Direct link to missing-sapling_api_key-environment-variable") Add `SAPLING_API_KEY` to the server's `env` configuration and restart the MCP client. Setting it only in a shell that does not launch the client may not pass it to the server. ### `npx` or `node` is not found[​](#npx-or-node-is-not-found "Direct link to npx-or-node-is-not-found") Install Node.js 18 or newer and restart the MCP client so it receives the updated system path. ### `402 Payment Required`[​](#402-payment-required "Direct link to 402-payment-required") The API key may be expired, inactive, or over its monthly billing limit. Review [API access and payment-required responses](https://sapling.ai/docs/api/api-access.md#payment-required-responses). ### Claude Code denies a Sapling tool call[​](#claude-code-denies-a-sapling-tool-call "Direct link to Claude Code denies a Sapling tool call") If a `sapling_*` tool call is blocked with a permission error rather than an approval prompt, add an allow rule for the tool: run `/permissions` and allow `mcp__sapling` (all Sapling tools) or `mcp__sapling__<tool_name>` (one tool), then retry. See [Tool permissions](#tool-permissions). ### The hosted server returns `401`[​](#the-hosted-server-returns-401 "Direct link to the-hosted-server-returns-401") The request carried no credential, or the token expired and the client did not renew it. Reconnect the server in your client to sign in again, or check the `Authorization: Bearer` header if you configured a key directly. ### A tool times out[​](#a-tool-times-out "Direct link to A tool times out") For unusually long API calls, set `SAPLING_MCP_TIMEOUT_MS` to a larger positive number. For large documents, follow the chunking guidance on the relevant API page. ## Security and data handling[​](#security-and-data-handling "Direct link to Security and data handling") Neither server logs nor persists your API key or submitted text. The hosted server stores no text and no credentials: it forwards your token or key to the Sapling API with the request and keeps nothing afterward. Tool inputs are sent to the configured Sapling API endpoint for processing. Review [Data Processing](https://sapling.ai/docs/data.md) and contact Sapling if you require no-data-retention or an on-premises deployment. If you set `SAPLING_API_BASE_URL`, the server sends your API key and submitted text to that endpoint. Point it only at a Sapling endpoint you trust, such as your own on-premises deployment. Releases are published on npm as [`@saplingai/mcp-server`](https://www.npmjs.com/package/@saplingai/mcp-server). --- # On-Premises Grammar Checkers and Language Model Assistants ## Language Model Assistants[​](#language-model-assistants "Direct link to Language Model Assistants") Language model assistants (or writing assistants), including grammar checkers such as Microsoft Editor and Grammarly, can help individuals and business teams: 1. Keep messages error-free and professional. 2. Save time, especially if they have to compose personalized replies. For business teams, such systems also have the ability to: 1. Ensure consistent messaging across a team. 2. Provide managers with analytics so that they can give feedback to their team members. <br /> [Contact Us](https://sapling.ai/contact?utm_campaign=apidocs) ## Software as a Service (SaaS)[​](#software-as-a-service-saas "Direct link to Software as a Service (SaaS)") Today, most software services, including services such as Microsoft Editor and Grammarly, are cloud-based Software as a Service (SaaS). This has many benefits, including: 1. Little upfront commitment is required. The client does not need to purchase extra servers to roll out the service, and costs are month-to-month or year-to-year. 2. The service continuously improves. As upgrades and new features are added, they automatically become available to users. 3. As SaaS providers handle deployment and integration, usually very little developer time is required from the client and it's assumed that the SaaS provider will handle scaling. As vendors may handle sensitive customer data, SaaS services should also be checked for security standards and compliance. This includes commonly agreed upon standards such as American Institute of CPAs (AICPA) SOC 2 certification (which includes two types, Type I and Type II) as well as the ISO 27001 certification. ## Regulations[​](#regulations "Direct link to Regulations") Requirements go beyond such external audits, however. You've likely already encountered regulations such as: * European General Data Protection Regulation ([GDPR](https://gdpr-info.eu/)) * California Consumer Privacy Act ([CCPA](https://oag.ca.gov/privacy/ccpa)), or * Philippines Data Privacy Act of 2012. <br /> ![Common compliance standards (SOC 2, HIPAA, PCI DSS)](/img/comp/compliance-row.png) A few of the common compliance standards. <br /> Such regulations impose requirements on businesses that process sensitive customer information, including requirements on data transfer, storage, documentation, and user consent. More regulations are looming. Further, some industries are unable to share data without meeting compliance standards. For example: * Healthcare: [HIPAA](https://www.hhs.gov/hipaa/index.html) (Health Insurance Portability and Accountability Act) compliance is required for businesses for the secure processing of PHI (protected health information) / ePHI (electronic protected health information). Sapling offers HIPAA-compliant spelling and grammar checkers in the cloud or self-hosted. * Finance: [PCI DSS](https://www.pcisecuritystandards.org/) (Payment Card Industry Data Security Standard) is a strict security standard for organizations that handle credit card information. Despite the convenience of SaaS, self-hosted/on-premises deployments may make sense for your business depending on regulations and business requirements in order to keep data security and privacy in-house. Assuming you're looking for one, which vendor should you go with? This article covers some of the different vendors, benefits and drawbacks of each, and the factors that should go into your decision. tip Spoiler: If you're looking for a vendor that will meet your requirements, you can check out Sapling's [integrations](https://sapling.ai/integrations?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs) or [contact us](https://sapling.ai/contact?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs). ## Providers[​](#providers "Direct link to Providers") Some of the aforementioned cloud-based grammar checkers like Microsoft Editor and Grammarly do not have self-hosted or on-premises offerings. Here are some common grammar checkers/writing assistant tools that offer self-hosted deployments: * Grammark * LanguageTool * Sapling * WebSpellChecker Let's go through the offerings one at a time. ### Grammark[​](#grammark "Direct link to Grammark") Grammark is a grammar checker tool created by Mark Fullmer. It currently includes over 7000 grammar rules in its database. The source code is available on [GitHub](https://github.com/markfullmer/grammark), making it an open source option. Grammark is primarily implemented in JavaScript, meaning that it should be easy to get running in the browser. However, given it uses a database of rules, it is likely restricted to certain types of errors, but not others. For example, on the Grammark webpage it states that subject-verb agreement errors are not handled. In the "Resources" section of the Grammark website, a list of error mappings is provided. ### LanguageTool[​](#languagetool "Direct link to LanguageTool") LanguageTool is proofreading software originally developed by Daniel Naber and released in 2005 as part of his 2003 thesis. LanguageTool's core system is free and open-source. Unlike systems such as Grammarly, languages supported include English, German, Spanish, French, and Portuguese. For teams with 250 or more uses, LanguageTool provides an on-prem option. With its ability to support multiple languages, however, comes some limitations in the types of suggestions that LanguageTool offers. On benchmarks, we have found that its database of rules does not provide as good coverage as systems such as Sapling. ### Sapling[​](#sapling "Direct link to Sapling") Sapling is the language model copilot for businesses. Its technology is based on research published in the Stanford AI lab. The most recently developed of the solutions described here, Sapling is especially geared towards business teams such as customer support teams or inside sales teams. It includes not only grammar checking and other writing assistance software for agents, but also a dashboard with actionable insights for managers. In addition to its focus on customer-facing teams, Sapling provides rich writing suggestions through its use of machine learning models trained on millions of sentences. Sapling also offers an on-prem option for businesses. As of this writing, it is not open source. ### WebSpellChecker[​](#webspellchecker "Direct link to WebSpellChecker") WebSpellChecker has been available as SpellChecker.net since at least 2000, but was refounded as WebSpellChecker in 2016. WebSpellChecker is an add-on for rich text editors that provides spelling and grammar check functionality. WebSpellChecker works with common editors such as CKEditor, TinyMCE, and Quill. It has dictionaries for over 100 languages, and supports self-hosted deployments. As of this writing, additional suggestions beyond spell checking and browser integrations do not appear to be publicly available. ## Which solution is best?[​](#which-solution-is-best "Direct link to Which solution is best?") | Provider | Benefits | Drawbacks | | ------------------------------------ | ------------------------------------------------------------------ | ---------------------------------------------------------------- | | WebSpellChecker | Multiple languages, integration with rich text editors | Non-spelling suggestions limited, browser integrations not found | | Sapling | Powerful AI models, designed for business teams, many integrations | Not open source | | LanguageTool | Multiple languages, open source option, many integrations | Based on database of rules | | Grammark | Open source, JS library | Based on database of rules, not many integrations | | Comparison of self-hosted providers. | | | For those looking for an self-hosted/on-premise grammar checker similar to Grammarly or Microsoft Editor, the best solution depends on your use case: * For those who need support for *multilingual support*, we recommend LanguageTool or Sapling. * For those looking to integrate with specific editors and mainly want *spell check*, we suggest trying WebSpellChecker. * For those keen on open source and are looking for a system that is implemented in *JavaScript*, we recommend Grammark. * For businesses with *customer-facing teams*, we recommend Sapling (us). ## Conclusion[​](#conclusion "Direct link to Conclusion") We recommend you reach out to the different vendors and make your own comparison. Obviously, we're biased. You can reach out to us for a trial here: [https://sapling.ai/contact](https://sapling.ai/contact?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs) Or give Sapling a try with one of our [integrations](https://sapling.ai/integrations?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs). <br /> Other tools? Let us know: `team@sapling.ai` --- # Overview ## Self-Hosting[​](#self-hosting "Direct link to Self-Hosting") [Contact Us](https://sapling.ai/contact?utm_campaign=apidocs) Ensure security and complete control of your data with Sapling's self-hosted, on-premises offering. Is your company in the financial services, healthcare, or other highly-regulated sector? Get the benefits of Sapling while meeting HIPAA, PCI, and other data compliance standards. ## Supported Products[​](#supported-products "Direct link to Supported Products") * Most API endpoints (contact us). * [Edits / Language Quality](https://sapling.ai/comp?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs) * [Spellcheck](https://sapling.ai/docs/api/spellcheck/) * [Autocomplete](https://sapling.ai/complete?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs) * [Snippets](https://sapling.ai/snippets-landing?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs) * [Suggest](https://sapling.ai/suggest?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs) ## Example Industries[​](#example-industries "Direct link to Example Industries") * **Healthcare**: Protect the privacy and security of patient information and avoid sharing data with another third party. * **Insurance**: Ensure personally identifiable information (PII) is safe and use audit logs to inspect all data access. * **Financial services**: Our data retention options ensure that no credit card or other financial information is ever stored or transmitted. ## Setup[​](#setup "Direct link to Setup") [Contact us](https://sapling.ai/contact?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs) for detailed documentation on and steps on getting started. You or a member of your team will need to have some familiarity with containers and virtualization. --- # OpenAPI Specification Sapling publishes a machine-readable [OpenAPI 3.1](https://spec.openapis.org/oas/v3.1.0) description of its REST API: ``` https://api.sapling.ai/openapi.json ``` The same document is also served at `https://api.sapling.ai/api/v1/openapi.json`. Fetching it does not require an API key. It can be loaded cross-origin from any website, and it is cached for up to an hour. ``` curl -s https://api.sapling.ai/openapi.json -o sapling-openapi.json ``` ## What it covers[​](#what-it-covers "Direct link to What it covers") The spec describes the public endpoints you can call with an API key: * **Language endpoints:** grammar and spelling ([edits](https://sapling.ai/docs/api/edits-overview.md), [spellcheck](https://sapling.ai/docs/api/spellcheck.md)), rephrase and paraphrase, summarize, translate, simplify, autocomplete, AI-content detection (text, PDF and Word files), language detection, PII, named entities, content safety, profanity, tone, sentiment, statistics, writing quality, SEO, classification, extraction, style guides, and similar terms. * **Supporting endpoints:** accept and reject feedback for edits and completions, the [custom dictionary](https://sapling.ai/docs/api/dictionary.md), [custom mappings and filters](https://sapling.ai/docs/api/custom-mappings.md), saved style-guide rule sets, [PDF and Word text extraction](https://sapling.ai/docs/api/files.md) and [chunking](https://sapling.ai/docs/api/chunk.md), account status, [usage reporting](https://sapling.ai/docs/api/api-reporting.md), and version and liveness probes. Operations are grouped by tag (`proofreading`, `generation`, `detection`, `analysis`, `files`, `account`, `service`), so generated clients get one API class per group. [Team Management](https://sapling.ai/docs/api/team-user-management.md) endpoints are not in the spec yet. Each operation lists its request body schema, authentication options, and the status codes and headers it can return: rate-limit headers, `402 Payment Required`, `429` backoff, and endpoint-specific cases such as the `502` from model-backed endpoints. Success responses are described at the level of the top-level JSON shape. For field-by-field response documentation and examples, see each endpoint's page in these docs. The spec links to them. Endpoints that accept a batch of texts declare both input forms. Their request schemas take exactly one of `text` (a single string) or `texts` (a list of 1–10 strings), and their success schemas include the `results` list that a batch request returns. The spec's top-level description summarizes the shared batch contract, and each endpoint's page documents it in a **Batch Requests** section (for example, [AI detection](https://sapling.ai/docs/api/detector.md#batch-requests)). The batch form is available for AI detection, classification, extraction, language detection, named entities, writing quality, content safety, simplification, style guides, summarization, and translation. The `operationId` of each language endpoint matches the corresponding tool name on the [Sapling MCP Server](https://sapling.ai/docs/mcp.md) (for example, `sapling_grammar_check` for `POST /api/v1/edits`), so agents can cross-reference the two. ## Authentication[​](#authentication "Direct link to Authentication") The spec declares an HTTP bearer scheme. Send your [API key](https://sapling.ai/docs/api/api-access.md) as `Authorization: Bearer YOUR_API_KEY`. As with every Sapling request, you can instead send the key as a `key` parameter — in the JSON body of operations that take one, or in the query string of the `GET` and `DELETE` operations. Each operation's schema declares the `key` parameter in the right location, so the spec marks the bearer header as optional. ## Using the spec[​](#using-the-spec "Direct link to Using the spec") ### Postman or Insomnia[​](#postman-or-insomnia "Direct link to Postman or Insomnia") In Postman, choose **Import → Link** and paste `https://api.sapling.ai/openapi.json`. Postman creates a collection with one request per endpoint. Set the collection's authorization to **Bearer Token** with your API key. In Insomnia, choose **Create → Import → URL** and paste the same address. ### Browse it interactively[​](#browse-it-interactively "Direct link to Browse it interactively") Open the spec in [Swagger Editor](https://editor.swagger.io/?url=https://api.sapling.ai/openapi.json) to browse every operation and its schemas. You can also send test requests from the **Try it out** panel after you enter your API key under **Authorize**. ### Generate a typed client[​](#generate-a-typed-client "Direct link to Generate a typed client") Any OpenAPI 3.1 code generator works. For example, with [openapi-generator](https://openapi-generator.tech/): ``` # Python npx @openapitools/openapi-generator-cli generate \ -i https://api.sapling.ai/openapi.json -g python -o sapling-client # TypeScript (fetch) npx @openapitools/openapi-generator-cli generate \ -i https://api.sapling.ai/openapi.json -g typescript-fetch -o sapling-client ``` Or generate TypeScript types only with [openapi-typescript](https://openapi-ts.dev/): ``` npx openapi-typescript https://api.sapling.ai/openapi.json -o sapling.d.ts ``` Sapling also maintains hand-written [Python and JavaScript SDKs](https://sapling.ai/docs/sdk.md). Use those unless you need a client in another language. ### ChatGPT Actions and agent frameworks[​](#chatgpt-actions-and-agent-frameworks "Direct link to ChatGPT Actions and agent frameworks") Many agent frameworks can turn an OpenAPI document into callable tools: * **Custom GPTs (ChatGPT Actions):** In the GPT editor, add an action, choose **Import from URL** and paste the spec URL. Under **Authentication**, choose **API Key → Bearer** and enter your Sapling API key. GPT Actions cap the number of operations one action can hold. If the import is rejected, or you want a focused GPT, delete the operations you don't need from the imported schema. * **LangChain, LlamaIndex, Semantic Kernel, and similar frameworks** ship OpenAPI toolkits that accept the spec URL. * If your assistant supports the Model Context Protocol, the [Sapling MCP Server](https://sapling.ai/docs/mcp.md) is usually the simpler route. It exposes the same language endpoints as ready-made tools. Tool calls made this way are ordinary API requests. They count against your API key's rate limits, quota, and billing like any other call. ## Rate limits and errors[​](#rate-limits-and-errors "Direct link to Rate limits and errors") The spec's top-level description documents the shared contract, and each operation declares the exact statuses it can return: * Rate-limited operations return `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset`. `X-RateLimit-Reset` is the number of seconds until the rate limit resets, not a timestamp. * `429` responses carry `Retry-After` and a numeric `retry_after` body field. If `retry_after` is missing, the request is too large to ever succeed, so don't retry it unchanged. * Errors are JSON with a `msg` field. That includes request-validation `400`s, `401`s, unknown paths (`404`), wrong methods (`405`, which also sends an `Allow` header) and `500`s. The 4MB `413` uses `message`/`result` keys instead. Treat the status code as authoritative. * After a `402` or `429`, call [Account Status](https://sapling.ai/docs/api/account-status.md) (`sapling_account_status`) for a machine-readable `blocked_reason` and `retry_after`. See [Rate Limits & Errors](https://sapling.ai/docs/rate_limits.md) for the limits themselves, what each status means, and a retry policy. ## Self-hosted deployments[​](#self-hosted-deployments "Direct link to Self-hosted deployments") On-premises deployments running a current Sapling release serve their own copy at `https://<your-sapling-host>/openapi.json`. That copy's `servers` entry points to your deployment rather than `api.sapling.ai`, so clients generated from it send requests (and your text) to your own infrastructure. If your deployment sits behind a proxy that hides its public hostname, your administrator can set the `OPENAPI_SERVER_URL` environment variable on the web service to override the advertised address. A deployment that disables API rate limiting serves a spec without rate-limit headers or `429` responses. --- # Sapling Playground We noticed that "playground" is a popular term for AI apps in search traffic analytics. ## TODO[​](#todo "Direct link to TODO") * <!-- --> Simple UI for edits endpoints * <!-- --> Live SDK demos * <!-- --> LLM Demo --- # Rate Limits & Errors ## Limits[​](#limits "Direct link to Limits") Sapling's API is rate-limited based on the subscription of the API key holder. * For free trials, the limit is 50,000 characters every 24 hours and 250,000 characters/month. Free-trial accounts also have a daily character allowance shared across all endpoints. When it runs out, requests return `402` until the next UTC midnight. * For subscribed users, this limit is currently 10 million characters every 24 hours and 25 million characters/month. * Each of these limits can be raised – contact us. How the limits are applied: * **Per key and per endpoint.** Each endpoint keeps its own allowance for each API key, so heavy use of one endpoint doesn't slow down another. * **Continuous refill.** The allowance refills evenly over its window instead of resetting all at once. For example, a limit of 50,000 characters every 24 hours gets back roughly 35 characters per minute. * **Batches.** A batch request (`texts`) counts the combined length of its items. * **Model-backed endpoints.** Classify, extract, inclusive language, named entities, simplify, style guide, summarize and translate count at least 100 characters per request, and a batch counts that minimum once per item. Many very short requests therefore use more allowance than their length alone. An AI detector URL check counts at least 5,000 characters. * **Monthly quota.** The monthly figure is a per-key quota that resets at the start of each UTC month. To see where a key stands, including why it is blocked and for how long, call [Account Status](https://sapling.ai/docs/api/account-status.md). ## Rate-limit headers[​](#rate-limit-headers "Direct link to Rate-limit headers") Rate-limited endpoints report the state of your allowance on each response they serve, including `429`s: | Header | Meaning | | ----------------------- | --------------------------------------------------------------------------------------------------- | | `X-RateLimit-Limit` | Size of this endpoint's allowance for your key, in characters. | | `X-RateLimit-Remaining` | Characters currently available. | | `X-RateLimit-Reset` | Seconds until the allowance is completely full again. This is a number of seconds, not a timestamp. | Use these headers to pace a batch job before you hit a `429`. They are omitted for keys that are configured without a rate limit. They are also omitted on self-hosted deployments that turn API rate limiting off. ## Error format[​](#error-format "Direct link to Error format") Errors are JSON objects with a human-readable `msg` field: ``` {"msg": "Invalid API key."} ``` This applies to request-validation errors, authentication errors, unknown paths, wrong HTTP methods and unexpected server errors. A few responses differ: * A request body over 4MB (`413`) uses `message` and `result` keys instead of `msg`. * `/api/v1/edits` can answer a temporary internal outage with an empty JSON array (`[]`) and status `400`. * Request-validation errors prefix `msg` with `400 Bad Request:`, for example `"400 Bad Request: Missing text argument"`. Treat the status code as authoritative and use `msg` for logs and messages to users. The [OpenAPI specification](https://sapling.ai/docs/openapi.md) lists the exact statuses each endpoint can return. ## Status codes[​](#status-codes "Direct link to Status codes") | Status | Meaning | Retry? | | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `400` | The request is invalid: malformed JSON, or a missing or invalid argument. | No. Fix the request. *Exception:* the safety, language detection, AI detector, rephrase, tone and sentiment endpoints also answer a temporary model failure with a `400` whose `msg` starts with `Unexpected error`. That case can be retried after a short delay. | | `401` | Missing or invalid API key. | No. | | `402` | Payment required. See [below](#402-payment-required). | Depends on the reason. | | `403` | The request isn't allowed for this key. For example, a `team_id` names a team the key's user doesn't belong to, or an MCP OAuth token was used on an endpoint outside the MCP tool set. | No. | | `404` | Unknown path, or a named resource (such as a saved style-guide `ruleset`) doesn't exist. | No. | | `405` | Wrong HTTP method. The `Allow` header lists the methods the path supports. | No. | | `413` | Request body over 4MB. | No. Split the text. | | `429` | Rate limited. See [below](#429-rate-limited). | Usually, after waiting. | | `500` | Unexpected server error. | Once or twice with backoff. Report persistent errors to <support@sapling.ai>. | | `502` | A model-backed endpoint's upstream model call failed. Nothing is cached or billed. | Yes, after a short delay. | | `503` | Temporarily overloaded. | Yes. Wait for the `Retry-After` interval. | ### 402 Payment Required[​](#402-payment-required "Direct link to 402 Payment Required") A `402` has one of two meanings: * **The block is permanent until you act.** The key or subscription has expired, prepaid credits are used up, or the account reached its monthly billing limit. Retrying won't help, so fix the account in your [API settings](https://sapling.ai/api_settings?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs). The response carries `Retry-After: 3600` only as a hint not to retry in a tight loop. See [payment-required responses](https://sapling.ai/docs/api/api-access.md#payment-required-responses). * **The block is temporary.** A free-trial account used up its daily allowance. The block clears at the next UTC midnight. Both `Retry-After` and a numeric `retry_after` body field give the seconds until then. The presence of `retry_after` is what tells the two cases apart. ### 429 Rate Limited[​](#429-rate-limited "Direct link to 429 Rate Limited") A `429` also comes in a few forms. Check the body's `retry_after` field: * **`retry_after` is present.** You used up your allowance for the moment. Wait that many seconds, which is also sent as the `Retry-After` header, then retry. This covers two cases: <!-- --> * The short-term allowance is used up. `msg` starts with `Rate limited. Capacity used.` * The key's monthly quota is used up. `msg` starts with `Rate Limited. Monthly quota used.`, and `retry_after` counts down to the start of the next UTC month. * **`retry_after` is absent.** This request can never succeed as sent, because it is larger than the key's entire allowance. It also happens when a request without an API key goes over the free character cap on rephrase or summarize. Don't retry unchanged. Split the text or send an API key. * **File uploads.** The file endpoints allow 60 uploads per minute and send no `Retry-After` or rate-limit headers. Back off for a minute and retry. ## Retrying requests[​](#retrying-requests "Direct link to Retrying requests") A simple, safe policy: 1. Retry these responses: <!-- --> * a `429` or `402` whose body includes `retry_after` (a short-term limit, the monthly quota, or the free-trial daily allowance) * `500`, `502` and `503` responses * the `400 Unexpected error` case described above 2. Wait at least `Retry-After` seconds when it's present. Otherwise use exponential backoff starting at about one second. 3. Give up after a few attempts, or when the wait is longer than your job can tolerate. A trial `402` can mean waiting until UTC midnight, and a monthly-quota `429` until the next month. Never retry a `429` or `402` without `retry_after` in a loop. 4. File uploads are the exception. Their `429` never has `retry_after`, so back off for about a minute and retry. The helper below is for the JSON endpoints. It gives up rather than sleep longer than `max_wait` seconds. ``` import time import requests RETRYABLE = {500, 502, 503} def post_with_retries(url, payload, attempts=4, max_wait=300): delay = 1 for attempt in range(attempts): response = requests.post(url, json=payload) try: body = response.json() if response.headers.get('Content-Type', '').lower().startswith('application/json') else {} except ValueError: body = {} if response.status_code < 400: return body transient_400 = (response.status_code == 400 and isinstance(body, dict) and str(body.get('msg', '')).startswith('Unexpected error')) can_wait = (response.status_code in (402, 429) and isinstance(body, dict) and 'retry_after' in body) if attempt == attempts - 1 or not (response.status_code in RETRYABLE or transient_400 or can_wait): response.raise_for_status() try: sleep_time = float(response.headers.get('Retry-After', delay)) except ValueError: sleep_time = delay if sleep_time > max_wait: response.raise_for_status() time.sleep(sleep_time) delay *= 2 ``` ### Partial results[​](#partial-results "Direct link to Partial results") The [Edits](https://sapling.ai/docs/api/edits-overview.md#response-parameters) and [Spellcheck](https://sapling.ai/docs/api/spellcheck.md#response-parameters) endpoints don't fail a whole document because a few sentences couldn't be processed. Under heavy load they answer `200` with every edit that did complete, plus a `degraded` object: ``` { "degraded": { "sentences_degraded": 1, "sentences_total": 12, "reason": "model_timeout" } } ``` The affected sentences may be missing some or all of their edits, and `degraded` only counts them; it doesn't say which ones they are. The edits returned are valid, but you can't tell an affected sentence from one that was fully checked, so don't assume the response is complete. It isn't an error, so the helper above returns it as-is. If you need every sentence checked, retry the whole request when `degraded` is present, with the same backoff as a `503`. A request where no sentence could be checked gets a `503` with `Retry-After` instead. If you keep running into rate limits, contact `support@sapling.ai` and we'll see if we can help. --- # Sandbox & Testing Sapling has no separate sandbox host. The sandbox is a **free trial key** used against the production API at `https://api.sapling.ai`, no credit card required. ## Get a test key[​](#get-a-test-key "Direct link to Get a test key") 1. [Register](https://sapling.ai/user/register?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs) for a Sapling account. 2. Open the [API settings dashboard](https://sapling.ai/api_settings?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs) and click **Generate Key**. The key starts a free trial: | | Free trial key | | ----------------------- | -------------------------------------------------------- | | Cost | Free. No credit card. | | Expiry | 30 days after the key is generated. | | Keys per account | One. | | Per-endpoint allowance | 50,000 characters every 24 hours, refilled continuously. | | Monthly quota | 250,000 characters per key. | | Daily account allowance | Shared across all endpoints. It resets at UTC midnight. | When a limit is reached, the API returns `429` (endpoint allowance or monthly quota) or `402` (daily account allowance), and the JSON body explains why. See [Rate Limits & Errors](https://sapling.ai/docs/rate_limits.md) for status codes and wait times. [Account Status](https://sapling.ai/docs/api/account-status.md) shows exact daily allowance and how much is left (`trial.daily_limit_chars` and `trial.chars_used_today`). The same key works for the REST API, the [SDKs](https://sapling.ai/docs/sdk.md) and the [MCP server](https://sapling.ai/docs/mcp.md). ## API checks[​](#api-checks "Direct link to API checks") These calls are not billed; uUse them to test connectivity and credentials. | Call | Key | What it tells you | | ------------------------------------------------------------ | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `GET https://api.sapling.ai/api/v1/liveness` | Not needed | The API is up. Returns `{"msg": "alive"}`. | | `GET https://api.sapling.ai/openapi.json` | Not needed | The machine-readable [OpenAPI spec](https://sapling.ai/docs/openapi.md) of every documented endpoint. | | `GET https://api.sapling.ai/api/v1/account/status?key=<key>` | Required | Whether the key can make billable calls, and if not, why and for how long. See [Account Status](https://sapling.ai/docs/api/account-status.md). | ``` curl https://api.sapling.ai/api/v1/version curl "https://api.sapling.ai/api/v1/account/status?key=$SAPLING_API_KEY" ``` ## Calls that store/modify state[​](#calls-that-storemodify-state "Direct link to Calls that store/modify state") Every documented endpoint not listed here only reads the text you send and returns a result. | Operation | What it changes | | ------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- | | `POST` / `DELETE` [`/api/v1/dictionary`](https://sapling.ai/docs/api/dictionary.md) | The account's custom dictionary. | | `POST` / `DELETE` [`/api/v1/custom_mapping`](https://sapling.ai/docs/api/custom-mappings.md) | The account's custom mappings. | | `POST` / `DELETE` `/api/v1/custom_filter` | The account's custom filters. | | `POST` / `DELETE` [`/api/v1/styleguide/rulesets`](https://sapling.ai/docs/api/styleguide.md) | Saved style-guide rule sets, for the account or, with a team key, the team. | | `POST /api/v1/edits/{edit_hash}/accept`, `/reject` and `POST /api/v1/complete/{completion_hash}/accept` | Record feedback on a suggestion. It appears in [usage reports](https://sapling.ai/docs/api/api-reporting.md). | | `POST` [`/api/v1/semsearch/create`](https://sapling.ai/docs/api/semsearch.md) and `DELETE /api/v1/semsearch/<entry_id>` | A team's [semantic search](https://sapling.ai/docs/api/semsearch.md) entries. (The `POST /api/v1/semsearch` query is read-only.) | | [Team](https://sapling.ai/docs/api/team-user-management.md) endpoints (`/api/v1/team/...`) | Real team members, dictionaries, mappings and snippets. | The dictionary, custom mappings and custom filters belong to the account that owns the key. Every key on that account and the Sapling dashboard share them, and they change the results of later [Edits](https://sapling.ai/docs/api/edits-overview.md) calls. ## Test data[​](#test-data "Direct link to Test data") Request text is handled as described in [Data Processing](https://sapling.ai/docs/data.md). A no-data-retention option is available: [contact us](https://sapling.ai/contact?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs) to enable it. ## Automated tests and agents[​](#automated-tests-and-agents "Direct link to Automated tests and agents") * **Unit tests**: mock Sapling's responses. The [OpenAPI spec](https://sapling.ai/docs/openapi.md) gives exact response schemas to build fixtures from. * **Integration tests**: keep them few and short, since each call uses allowance. Run `account/status` first so a spent allowance fails fast with a clear reason, not an unexpected error. * **Retries**: handle `429` and `402` as described in [Rate Limits & Errors](https://sapling.ai/docs/rate_limits.md). A trial `402` can mean waiting until UTC midnight. * **Agents**: every [MCP server](https://sapling.ai/docs/mcp.md) tool processes text and returns a result. None of them write to the dictionary, custom mappings, rule sets or team settings, so an agent can be given a trial key and explore freely. ## Moving to production[​](#moving-to-production "Direct link to Moving to production") [Subscribe](https://sapling.ai/api-checkout?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs) or buy [prepaid API credits](https://sapling.ai/docs/api/api-access.md#prepaid-api-credits). Your existing key keeps working: its expiry is removed and its limits are raised to the production defaults, so you don't need to change any configuration. After that, you can generate more than one key. Keep a separate key for staging or CI, and give each key a name in the [API settings dashboard](https://sapling.ai/api_settings?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs). Usage for each key is available in [API usage reports](https://sapling.ai/docs/api/api-reporting.md). --- ## [📄️Overview](https://sapling.ai/docs/sdk/overview.md) [Sapling has SDKs that assist in grammar checking text for Python and JavaScript, and an HTTP API that is language agnostic.](https://sapling.ai/docs/sdk/overview.md) --- # Deploying to Production Here's a sample deployment setup using the JavaScript SDK along with a Python backend. This is of course just one way to set up a deployment; if you have any questions, hop on our [Discord](https://discord.gg/p5gDc684TX) and chat with us. ## Steps[​](#steps "Direct link to Steps") * Import the SDK by inserting it into a page as a JavaScript script. * Initialize with API key using the `Sapling.init` [method](https://sapling.ai/docs/sdk/JavaScript/frontend_documentation.md). * Observe any textarea or contenteditable DOM element using the `Sapling.observe` [method](https://sapling.ai/docs/sdk/JavaScript/frontend_documentation.md). * Set up a backend endpoint with your API key to request edits. Sample Quickstart * Save the [Sample HTML Frontend code](https://sapling.ai/docs/sdk/deploying.md#sample-html-frontend) to `sdk.html` and open in a browser. * Save the [Sample Python (Flask) code](https://sapling.ai/docs/sdk/deploying.md#sample-python-flask-backend) below to `sdk_flask.py`. * Update `<API_KEY>` in the Python code. * Then run the following in command line: ``` pip install flask requests flask_cors FLASK_APP=sdk_flask.py flask run --port 3000 ``` ## Sample HTML Frontend[​](#sample-html-frontend "Direct link to Sample HTML Frontend") ``` <html> <head> <script src="https://sapling.ai/static/js/sapling-sdk-v1.1.1.min.js"></script> </head> <body> <div contenteditable="true" id="editor" sapling-ignore="true">Lets get started!</div> <script type="text/javascript"> Sapling.init({ endpointHostname: 'http://localhost:3000', saplingPathPrefix: '/sapling', }); const contentEditable = document.getElementById('editor'); Sapling.observe(contentEditable); </script> </body> </html> ``` For more information please see the [JavaScript documentation](https://sapling.ai/docs/sdk/JavaScript/quickstart.md). ## Sample Python (Flask) Backend[​](#sample-python-flask-backend "Direct link to Sample Python (Flask) Backend") Here's the backend in Python (Flask). Adapt the code below according to the framework that you're using. ``` import requests from flask import Flask, jsonify, request from flask_cors import CORS app = Flask(__name__) CORS(app, origins='*') SAPLING_API_HOSTNAME = 'https://api.sapling.ai' API_KEY = '<api-key>' @app.route('/sapling/<path:path>', methods=['POST']) def sapling_proxy(path): request_url = f'{SAPLING_API_HOSTNAME}/{path}' args = request.get_json() args['key'] = API_KEY result = requests.post(request_url, json=args) result = result.json() return jsonify(result), 200 ``` --- ## [📄️CKEditor](https://sapling.ai/docs/sdk/Editors/ckeditor.md) [Sample Code](https://sapling.ai/docs/sdk/Editors/ckeditor.md) --- # CKEditor ## Sample Code[​](#sample-code "Direct link to Sample Code") You can find a example of using Sapling with CKEditor here: <https://github.com/saplingai/sapling-samples/blob/master/editors/ckeditor.html> ## Tips[​](#tips "Direct link to Tips") * The main thing to keep in mind is to use a callback to initialize Sapling *after* CKEditor has completed setting up the editable element. * Contact us if you need help initializing with elements within an `iframe`. --- # Froala ## Sample Code[​](#sample-code "Direct link to Sample Code") You can find a example of using Sapling with Froala here: <https://github.com/saplingai/sapling-samples/blob/master/editors/froala.html> ## Tips[​](#tips "Direct link to Tips") * The main thing to keep in mind is to use a callback to initialize Sapling *after* Froala has completed setting up the editable element. --- # Quill ## Sample Code[​](#sample-code "Direct link to Sample Code") You can find a example of using Sapling with Quill here: <https://github.com/saplingai/sapling-samples/blob/master/editors/quilljs.html> --- # TinyMCE ## Sample Code[​](#sample-code "Direct link to Sample Code") You can find a example of using Sapling with TinyMCE here: <https://github.com/saplingai/sapling-samples/blob/master/editors/tinymce.html> ## Tips[​](#tips "Direct link to Tips") * The main thing to keep in mind is to use a callback to initialize Sapling *after* TinyMCE has completed setting up the editable element. --- # Electron Electron is a library/framework for building cross-platform desktop apps with JavaScript, HTML, and CSS. [![Electron](/img/electron.svg)](https://https://www.electronjs.org/) Here are some example applications built using Electron: * Discord * Asana * Figma * Notion * Microsoft Teams * Slack * WhatsApp * Signal ...and the list goes on. <br /> Sapling offers integrations with Electron apps through its SDK. You can use any of the options below to integrate with Electron. * [HTML SDK](https://sapling.ai/docs/sdk/HTML/quickstart.md) * [JavaScript SDK](https://sapling.ai/docs/sdk/JavaScript/quickstart.md) (includes React) ## Basic Debugging[​](#basic-debugging "Direct link to Basic Debugging") If Sapling does not work as expected, you can start Chrome Devtools by following the instructions [here](https://www.electronjs.org/docs/latest/tutorial/application-debugging). In essence, if your `BrowserWindow` object is called `mainWindow`, call `mainWindow.webContents.openDevTools()`. The starter code should have a line that you can simply uncomment. From there you'll be able to see error messages in the Console tab. If you see that the page refused to load the Sapling SDK script because of a Content Security Policy (CSP) issue and are unable to debug, contact us. Similarly let us know if you have any issues importing the Sapling module. --- # AI Detect HTML Quickstart Developer Key A rate-limited API developer key can be provisioned from the [Sapling API dashboard](https://sapling.ai/api_settings?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs). Developer keys allow for processing of 50,000 characters every 24 hours. [Subscribe](https://sapling.ai/api-checkout) for production access and [usage-based pricing](https://sapling.ai/docs/api/pricing). Here's a minimal webpage using the Sapling SDK that includes a button for AI Detection. This allows an easy way for developers to add AI content detection capabilities into their websites. Clicking this button runs Sapling's AI detector on the target content and displays an overall score as well as highlights sentences flagged as AI-generated. ![](/img/sdk/ai_detect_sdk.png) Screenshot of the AI Detection SDK in action ### Sample Code[​](#sample-code "Direct link to Sample Code") ``` <html> <head> <script src="https://sapling.ai/static/js/sapling-sdk-v1.1.1.min.js"></script> </head> <body> <div contenteditable="true" id="possible-ai-content" sapling-ignore="true"> Lets get started! </div> <button id="ai-detect-button">Check for AI Content</button> <script type="text/javascript"> Sapling.init({ key: '<api-key>', mode: 'dev', }); const contentEditable = document.getElementById('possible-ai-content'); Sapling.observe(contentEditable); const aiDetectButton = document.getElementById('ai-detect-button'); aiDetectButton.addEventListener('click', () => { Sapling.checkOnce(contentEditable, {aiDetect: true}); }); </script> </body> </html> ``` caution Do not serve a page like this publicly, as it exposes the API key. * During development, you can pass in the API key and use Sapling's endpoints by setting `mode: 'dev'` in the `init` function. * For production, do not pass the API key in JavaScript. Instead, include the API key in your backend and call Sapling's endpoints from the backend. This is the default setting with `mode: 'prod'`. * See our [Javascript Fullstack Quickstart](https://sapling.ai/docs/sdk/JavaScript/ai_detect_quickstart.md) for a complete example --- # HTML Quickstart Developer Key A rate-limited API developer key can be provisioned from the [Sapling API dashboard](https://sapling.ai/api_settings?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs). Developer keys allow for processing of 50,000 characters every 24 hours. [Subscribe](https://sapling.ai/api-checkout) for production access and [usage-based pricing](https://sapling.ai/docs/api/pricing). The complete JavaScript API can be found under [JavaScript -> Frontend Documentation](https://sapling.ai/docs/sdk/JavaScript/frontend_documentation.md). Below is a minimal webpage using the Sapling SDK. ``` <html> <head> <script src="https://sapling.ai/static/js/sapling-sdk-v1.1.1.min.js"></script> </head> <body> <div contenteditable="true" id="check-space" sapling-ignore="true">Lets get started!</div> <script type="text/javascript"> Sapling.init({ key: '<api-key>', mode: 'dev', }); const contentEditable = document.getElementById('check-space'); Sapling.observe(contentEditable); </script> </body> </html> ``` caution Do not serve a page like this publicly, as it exposes the API key. * During development, you can pass in the API key and use Sapling's endpoints by setting `mode: 'dev'` in the `init` function. * For production, do not pass the API key in JavaScript. Instead, include the API key in your backend and call Sapling's endpoints from the backend. This is the default setting with `mode: 'prod'`. * See our [Javascript Fullstack Quickstart](https://sapling.ai/docs/sdk/JavaScript/quickstart.md#fullstack-quickstart) for a complete example --- # Accessibility warning On May 9, 2024, the Department of Health and Human Services (HHS) Office for Civil Rights (OCR) published [Nondiscrimination on the Basis of Disability in Programs or Activities Receiving Federal Financial Assistance](https://www.federalregister.gov/documents/2024/05/09/2024-09237/nondiscrimination-on-the-basis-of-disability-in-programs-or-activities-receiving-federal-financial). The rule — effective July 8, 2024 — updates and expands Section 504 of the Rehabilitation Act of 1973 (Section 504). A summary Fact Sheet can be found [here](https://www.hhs.gov/civil-rights/for-individuals/disability/section-504-rehabilitation-act-of-1973/ocr-detailed-504-fact-sheet/index.html). In brief, the rule requires that entities receiving federal financial assistance ensure accessibility of their digital and information technology (IT) to people with disabilities. Accessible grammar checkers and writing assistants are helpful for those with disabilities such as visual impairment. To help entities that fall under the expanded Section 504 requirements effective July 8, 2024, Sapling offers the following web accessibility functionality with its SDK: ## Screen Reader Support[​](#screen-reader-support "Direct link to Screen Reader Support") For screen readers, Sapling includes the `aria-label` property on interactive elements. Contact us if you need support working with HHS disability regulations. ## Keyboard Navigation[​](#keyboard-navigation "Direct link to Keyboard Navigation") So that visually impaired users can use the Sapling interface, we have implemented keyboard shortcuts that allow users to tab through interactive elements and perform accept and ignore actions via the keyboard. ## Next Steps[​](#next-steps "Direct link to Next Steps") You can get started with Sapling's SDK using our [Quickstart Guide](https://sapling.ai/docs/sdk/JavaScript/quickstart.md). If you haven't registered for an account, start [here](https://sapling.ai/docs/api/api-access/). --- # Style Customization info For technical support, contact <support@sapling.ai>. Sapling's SDK UI elements support custom styling through flags and CSS. During SDK setup, the following attributes can be set as part of the `init`: ``` Sapling.init({ ... statusBadge: true, appearance: { compact: true, variables: { acceptText: 'Accept', // extended view only ignoreText: 'Ignore', badgeSize: 8, // px badgeMargin: 8, // px badgeColor: '#3f51b5', badgeErrorColor: '#990000', }, customCSS: '', }, }); ``` The elements and injected CSS is encapsulated by a Shadow DOM, and does not affect the rest of your page. ## CSS Styling Specificity[​](#css-styling-specificity "Direct link to CSS Styling Specificity") Default CSS styling uses direct ID selectors. More specific CSS selectors will override those. As an example, using the selector `#sapling-edit-controls #accept-text` instead of `#accept-text` should prioritize the override. You can read more about how this is handled here: [CSS Specificity Documentation](https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity) ### Compact Edit Component Structure[​](#compact-edit-component-structure "Direct link to Compact Edit Component Structure") The compact edit component is shown by default. The HTML is structured as follows: ![](/img/sdk/compact_edit_components.svg) ``` <sapling-controls> <div id="accept-row" class="edit-controls-row"> <div id="accept-button" tabindex="0"> <div class="icon-sapling-accept"></div> <div id="accept-text">${acceptText}</div> </div> </div> <div id="ignore-row" class="edit-controls-row"> <div id="ignore-button" tabindex="0"> <div class="icon-sapling-reject"></div> <div id="ignore-text">${ignoreText}</div> </div> </div> </sapling-controls> ``` ### Extended Edit Component Structure[​](#extended-edit-component-structure "Direct link to Extended Edit Component Structure") The extended edit component can be shown by initializing the SDK with `appearance.compact: false`. The HTML is structured as follows: ![](/img/sdk/extended_edit_components.svg) ``` <sapling-controls> <div id="type-row"> <div class="icon-sapling-type"></div> <div id="type-text"></div> </div> <div id="text-row"> <div id="original-text-row"> <div class="icon-sapling-original-text"></div> <div id="original-text"></div> </div> <div class="icon-sapling-right-arrow"></div> <div id="replacement-text-row"> <div class="icon-sapling-replacement-text"></div> <div id="replacement-text"></div> </div> </div> <div id="accept-row" class="edit-controls-row"> <div id="accept-button" tabindex="0"> <div class="icon-sapling-accept"></div> <div id="accept-text">${acceptText}</div> </div> </div> <div id="ignore-row" class="edit-controls-row"> <div id="ignore-button" tabindex="0"> <div class="icon-sapling-reject"></div> <div id="ignore-text">${ignoreText}</div> </div> </div> <div id="description-row" class="edit-controls-row"> <div class="icon-sapling-description"></div> <div id="description-text"></div> </div> </sapling-controls> ``` ### Badge Component Structure[​](#badge-component-structure "Direct link to Badge Component Structure") The badge is the circle status indicator in the bottom right of the textarea or content-editable. By default it is a circle with diameter `8px` and margins of `8px` from the bottom and right corner. The HTML is structured as follows: ``` <div class="sapling-badge-container"> <div class="sapling-status-container"> <div class="sapling-badge"></div> <div class="sapling-badge-spinner"></div> </div> </div> ``` Outside of the following attributes: * badgeSize * badgeMargin * badgeColor * badgeErrorColor You can also set other customizations through the `customCSS`. As an example, you can slow down the spinner with the following: ``` .sapling-badge-container .sapling-badge-spinner { animation: rotation 4s infinite linear; } ``` ## Example[​](#example "Direct link to Example") Here is an example of a green edit control in a dark-themed UI: ![](/img/sdk/customization_example.png) <br /> <br /> <br /> The example HTML to generate that is below (replace `API_KEY`): ``` <html> <head> <style type="text/css"> body, textarea { background-color: black; color: white; } </style> </head> <body> <textarea sapling-ignore="true" id="text-element" rows="10" cols="40" style="padding:20px"> Lets get started. </textarea> </body> <script src="https://sapling.ai/static/js/sapling-sdk-v1.1.1.min.js"></script> <script type="text/javascript"> var customCSS = ` #sapling-edit-controls #edit-controls-buttons { border: 0.5px solid #333; } #sapling-edit-controls #accept-button, #sapling-edit-controls #ignore-button { background-color: #111; } #sapling-edit-controls .icon-sapling-accept, #sapling-edit-controls #accept-text { color: green; } #sapling-edit-controls #accept-button:hover, #sapling-edit-controls #ignore-button:hover{ background-color: #222; } #sapling-edit-controls .icon-sapling-reject, #sapling-edit-controls #ignore-text { color: #565656; } #sapling-edit-controls #ignore-row { border-top: 0.5px solid #333; } `; Sapling.init({ key: '<api-key>', mode: 'dev', appearance: { customCSS } }); const textarea = document.getElementById('text-element'); Sapling.checkOnce(textarea); </script> </html> ``` --- # Flagging Issues in Text Sapling's API and SDK can be used in a variety of different ways to process text and automatically flag issues. These can range from grammatical mistakes to making writing more confident, concise, or inclusive. We list some of the issues that you can flag in your text as part of a processing pipeline, or integrated into an application that acts as an AI writing assistant for your users. To get started, you'll need to generate an API key following the steps [here](https://sapling.ai/docs/api/api-access.md). ## Contents[​](#contents "Direct link to Contents") 1. [Spell Checking](#spell-checking) 2. [Grammar Issues](#grammar-issues) 3. [Advanced Edits](#advanced-edits) 4. [AI Content Detection](#ai-content) 5. [Tone Issues](#tone) 6. [Diversity, Equity and Inclusion (DEI)](#diversity-equity-and-inclusion-dei) ## Spell Checking[​](#spell-checking "Direct link to Spell Checking") Spell checking is the quickest and cheapest API endpoint for flagging issues in text. You can check this page to see which languages are supported: [/docs/api/spellcheck](https://sapling.ai/docs/api/spellcheck.md). If you are looking to add spell checking functionality to a website or web application, you can use the [JavaScript SDK](https://sapling.ai/docs/sdk/JavaScript/quickstart.md). #### Sample Code[​](#sample-code "Direct link to Sample Code") ``` import requests key = '<api-key>' url = 'https://api.sapling.ai/api/v1/spellcheck' data = { 'key': key, 'text': 'Here is a recomendation.', 'session_id': 'Test Document UUID', } resp = requests.post(url, json=data) if 200 <= resp.status_code < 300: resp_json = resp.json() print('Edits: ', resp_json['edits']) else: print('Error: ', str(resp.status_code), resp.text) ``` #### Sample Output[​](#sample-output "Direct link to Sample Output") ``` [{ "id": "c7a85578-2391-5d9e-81b5-fc5c1574d07a", "replacement": "recommendation", "sentence": "Here is a recomendation.", "sentence_start": 0, "start": 10, "end": 23 }] ``` ## Grammar Issues[​](#grammar-issues "Direct link to Grammar Issues") Sapling can help suggest edits around grammatical errors and ensure that your writing is idiomatic and fluent. Check [http://sapling.ai/docs/api/edits-overview](https://sapling.ai/docs/api/edits-overview.md) to see which languages are supported. If you are looking to add grammar checking functionality to a website or web application, you can use the [JavaScript SDK](https://sapling.ai/docs/sdk/JavaScript/quickstart.md). #### Sample Code[​](#sample-code-1 "Direct link to Sample Code") ``` import requests key = '<api-key>' url = 'https://api.sapling.ai/api/v1/edits' data = { 'key': key, 'text': 'Lets get started!', 'session_id': 'Test Document UUID', } resp = requests.post(url, json=data) if 200 <= resp.status_code < 300: resp_json = resp.json() print('Edits: ', resp_json['edits']) else: print('Error: ', str(resp.status_code), resp.text) ``` #### Sample Output[​](#sample-output-1 "Direct link to Sample Output") ``` [{ "error_type": "R:OTHER", "general_error_type": "Other", "id": "aa5ee291-a073-5146-8ebc-c9c899d01278", "replacement": "Let's", "sentence": "Lets get started!", "sentence_start": 0, "start": 0 "end": 4, }] ``` ## Advanced Edits[​](#advanced-edits "Direct link to Advanced Edits") This is a category of writing suggestions that go beyond regular grammatical correctness. Advanced Edits are a set of more opionated recommendations for effective written communication. These suggestions may make more sense applied to a press release or a business email compared to a creative novel. Below is a list of configurable advanced edits categories and corresponding examples: #### Adverbs[​](#adverbs "Direct link to Adverbs") Adverbs can usually be replaced either with a stronger verb. He ran quickly towards the exit. -> He dashed towards the exit. #### Simplifications[​](#simplifications "Direct link to Simplifications") Consider replacing complex phrases with a simpler one with the same meaning. A number of people feel this way. -> Many people feel this way. #### Hard to read[​](#hard-to-read "Direct link to Hard to read") Sentences that are too long can be hard to read. Consider breaking them up for clarity. In the age of digital technology and social media, it's becoming increasingly important for individuals to be aware of their online presence and the impact that their actions and words can have on others. -> In the digital age, it's vital to be aware of how our online presence and actions can impact others. #### Passive Voice[​](#passive-voice "Direct link to Passive Voice") Sentences in the active voice are usually more concise and clear. The ball was kicked by him. -> He kicked the ball. #### Qualifiers[​](#qualifiers "Direct link to Qualifiers") Qualifiers lead to less confident communication. Consider removing them. I was wondering if we can meet tomorrow? -> Can we meet tomorrow? You can find a demo of advanced edits [here](https://sapling.ai/utilities/issue-highlighter?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs). * cURL * JavaScript * Python ``` curl -X POST https://api.sapling.ai/api/v1/edits \ -H "Content-Type: application/json" \ -d '{ "key":"<api-key>", "text":"The ball was kicked. Suddenly there was the sound of an aircraft.", "session_id": "test session", "advanced_edits": { "advanced_edits": true } }' ``` ``` import axios from 'axios'; async function run(text) { try { const response = await axios.post( 'https://api.sapling.ai/api/v1/edits', { key: '<api-key>', session_id: 'test session', text, advanced_edits: { advanced_edits: true, }, }, ); const {status, data} = response; console.log({status}); console.log(JSON.stringify(data, null, 4)); } catch (err) { const { msg } = err.response.data; console.log({err: msg}); } } run('The ball was kicked. Suddenly there was the sound of an aircraft.'); ``` ``` import requests from pprint import pprint key = '<api-key>' url = 'https://api.sapling.ai/api/v1/edits' data = { 'key': key, 'text': 'The ball was kicked. Suddenly there was the sound of an aircraft.', 'session_id': 'Test Document UUID', 'advanced_edits': { 'advanced_edits': True, }, } try: response = requests.post(url, json=data) if 200 <= response.status_code < 300: resp_json = response.json() edits = resp_json['edits'] pprint(edits) else: print('Error: ', str(response.status_code), response.text) except Exception as e: print('Error: ', e) ``` ### Sample Response[​](#sample-response "Direct link to Sample Response") ``` [ { "description": "Reduce the use of passive voice.", "end": 19, "error_type": "voice", "general_error_type": "Advanced Edit", "id": "81695ccc-2916-50ba-8b93-c3ed7bb55f54", "replacement": "", "sentence": "The ball was kicked.", "sentence_start": 0, "start": 9 }, { "description": "Reduce the use of adverbs.", "end": 8, "error_type": "adverbs", "general_error_type": "Advanced Edit", "id": "62cb63df-7c3d-52de-aab2-eded6924d689", "replacement": "", "sentence": "Suddenly there was the sound of an aircraft.", "sentence_start": 21, "start": 0 }, { "description": "Use simpler language where possible.", "end": 43, "error_type": "simplifications", "general_error_type": "Advanced Edit", "id": "a0367693-de21-524f-9a3e-abe7a62b46f6", "replacement": "plane", "sentence": "Suddenly there was the sound of an aircraft.", "sentence_start": 21, "start": 35 } ] ``` ## AI Content[​](#ai-content "Direct link to AI Content") The AI Content detector can be used to flag sentences or sections of text that were likely to have been generated by an AI language model. Depending on how the text is being used, AI content may not be allowed for legal, educational or regulatory reasons. Using an AI content detector can help reduce the costs of detecting such content. Details on the [AI detection API](https://sapling.ai/docs/api/detector.md) can be found in the API documentation. #### Sample Code[​](#sample-code "Direct link to Sample Code") ``` import requests from pprint import pprint key = '<api-key>' url = 'https://api.sapling.ai/api/v1/aidetect' data = { 'key': key, 'text': 'I am an artificial intelligence system designed to help people solve complex problems. My capabilities include natural language processing, machine learning, and predictive analytics.', } try: response = requests.post(url, json=data) if 200 <= response.status_code < 300: resp_json = response.json() pprint(resp_json) else: print('Error: ', str(response.status_code), response.text) except Exception as e: print('Error: ', e) ``` #### Sample Output[​](#sample-output "Direct link to Sample Output") ``` { "score": 0.9989707556330055, "ai_fraction": 1.0, "verdict": "ai", "segments": [ { "start": 0, "end": 183, "score": 0.985 } ], "low_confidence": true, "low_confidence_reasons": ["short_text"], "sentence_scores": [ { "score": 0.9764397893790885, "sentence": "I am an artificial intelligence system designed to help people solve complex problems.", "start": 0, "end": 86 }, { "score": 0.9923806397189778, "sentence": "My capabilities include natural language processing, machine learning, and predictive analytics.", "start": 87, "end": 183 } ], "text": "I am an artificial intelligence system designed to help people solve complex problems. My capabilities include natural language processing, machine learning, and predictive analytics." } ``` ## Tone[​](#tone "Direct link to Tone") Tone analysis is done on an overall level as well as on a per sentence level. We flag negative passages of text that could be interpreted negatively for review. If you or your users want to avoid coming off as angry or disapproving, this endpoint can help. More details on the tone api and all the tones classified can be found [here](https://sapling.ai/docs/api/tone.md). Here is an example of how the Tone API is used in the Sapling browser extension. Note how the negative sentence is highlighted. ![](/img/sdk/tone_ui1.png) This is an example of another UI component from the Sapling tone checker. You can display the overal analyzed tone of a piece of text. ![](/img/sdk/tone_ui2.png) #### Sample Code[​](#sample-code "Direct link to Sample Code") ``` import requests key = '<api-key>' url = 'https://api.sapling.ai/api/v1/tone' data = { 'key': key, 'text': 'You are amazing', } resp = requests.post(url, json=data) if 200 <= resp.status_code < 300: print(resp.json()) else: print('Error: ', str(resp.status_code), resp.text) ``` #### Sample Output[​](#sample-output "Direct link to Sample Output") ``` { "overall": [ [0.8885115385055542, "admiring", "😲"], [0.06413709372282028, "excited", "😀"], [0.02678699605166912, "approving", "👍"] ], "results": [ [ [0.8885115385055542, "admiring", "😲"], [0.06413709372282028, "excited", "😀"], [0.02678699605166912, "approving", "👍"] ] ], "sents": ["You are amazing"] } ``` ## Diversity, Equity and Inclusion (DEI)[​](#diversity-equity-and-inclusion-dei "Direct link to Diversity, Equity and Inclusion (DEI)") When writing in a way that is "DEI", we want to respect the linguistic viewpoints and cultures of different people. Inclusion in language refers to treating people with respect and making them feel included. While there is no absolute standard for DEI language, we can always avoid using certain terms that are considered racist or derogatory towards disadvantaged groups of people. Sapling breaks DEI into the following categories. Example of each are also given: * Gender Pronouns Prefer "they"/"them" over "he"/"him" * Gender Nouns Prefer "chairperson" over "chairman" * Gender Identity Prefer "transgender" over "transexual" * Disability Prefer "autistic" over "aspie" * Age Prefer "person born betwen 1946-1964" over "boomer" * Race Prefer "primary-secondary" over "master-slave" * Social Class Prefer "sex worker" over "prostitute" * Violence Prefer "situation room" over "war room" You can fine-tune your DEI settings in the language settings dashboard here: [https://sapling.ai/language\_settings](https://sapling.ai/language_settings?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs). A demo of DEI checking can be found [here](https://sapling.ai/utilities/issue-highlighter?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs). #### Sample Code[​](#sample-code "Direct link to Sample Code") ``` import requests key = '<api-key>' url = 'https://api.sapling.ai/api/v1/edits' data = { 'key': key, 'text': 'This is the master bedroom.', 'session_id': 'Test Document UUID', 'advanced_edits': { 'dei': True, }, } try: resp = requests.post(url, json=data) if 200 <= resp.status_code < 300: resp_json = resp.json() edits = resp_json['edits'] print('Edits: ', edits) else: print('Error: ', str(resp.status_code), resp.text) except Exception as e: print('Error: ', e) ``` #### Sample Output[​](#sample-output "Direct link to Sample Output") ``` [ { "description": "Use more inclusive language where possible.", "end": 18, "error_type": "dei", "general_error_type": "Advanced Edit", "id": "fa409ba8-a51c-5d9e-b007-1bde1ac6183a", "replacement": "primary", "sentence": "This is the master bedroom.", "sentence_start": 0, "start": 12 } ] ``` --- # Migrating from the Grammarly Text Editor SDK warning Important: The Grammarly Text Editor SDK will no longer be updated and will be deprecated on January 10, 2024. We suggest removing the integration from your app or website before that date. - [Grammarly](https://developer.grammarly.com/sdk-deprecation) The Grammarly Text Editor SDK will be discontinued on January 10, 2024 [(source)](https://developer.grammarly.com/sdk-deprecation). Current Grammarly for Developers applications will function normally until the discontinuation date, new registrations are halted, and developers with recent registrations have until July 26, 2023, to create new apps with the SDK. ## Grammarly SDK alternative[​](#grammarly-sdk-alternative "Direct link to Grammarly SDK alternative") This is a quick guide for developers using the Grammarly SDK who are interested in maintaining spelling or grammar check functionality in their web applications. The functionality of Sapling's AI Writing Assistant can be included in any application as a drop-in replacement for the Grammarly SDK. The AI-powered Sapling SDK offers not only spelling and grammar checking, but also additional features such as tone analysis, AI content detection, context-aware suggestions, multilingual support, and much more. If you're looking for a Grammarly SDK alternative, this guide will help you make the transition. # API Access Anyone can test out Sapling's API. To get started with the API, simply: 1. Register for a Sapling account [here](https://sapling.ai/user/register?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs). 2. Once you've registered and signed in, visit your Sapling [API settings dashboard](https://sapling.ai/api_settings?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs). 3. Click the Generate Key button under the "API" section of the settings pane. 4. Start using the API by following the API documentation. We recommend starting with the [Grammar Check API (Edits)](https://sapling.ai/docs/api/edits-overview.md) guide. The key starts a free trial that works as a sandbox: it is never charged, and almost every endpoint only reads the text you send. See [Sandbox & Testing](https://sapling.ai/docs/sandbox.md) for the trial limits, zero-cost health checks, and the few calls that change stored state. When making requests, you can authenticate by including your API key as a `key` parameter or by providing it as a bearer token in the `Authorization` header. If both are supplied, the `key` parameter takes precedence. A machine-readable [OpenAPI 3.1 specification](https://sapling.ai/docs/openapi.md) of the API is available at `https://api.sapling.ai/openapi.json`. Use it to import the API into Postman, generate a typed client in any language, or register Sapling endpoints as tools for an AI agent. ## Prepaid API credits[​](#prepaid-api-credits "Direct link to Prepaid API credits") You can purchase prepaid API credits from the **Billing** section of the [API settings dashboard](https://sapling.ai/api_settings?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs#billing). One credit equals one US dollar, and API usage draws down the balance using the same per-character prices described on the [API pricing](https://sapling.ai/docs/api/pricing) page. The dashboard shows purchased, consumed, and remaining credits. Sapling sends a low-balance warning once you have used the configured share (shown as `alert_threshold_percent` in [Account Status](https://sapling.ai/docs/api/account-status.md)) of the credits bought since the last warning, so a top-up after a warning starts a fresh count. A positive prepaid balance can activate or reactivate API access without a metered subscription; when the balance is depleted, API requests return `402 Payment Required` until more credits are purchased. If a card payment on your API invoice fails, Sapling can switch the account to prepaid credits only. [Account Status](https://sapling.ai/docs/api/account-status.md) then reports `credits_only: true`, and API requests are served only while you have a positive prepaid balance. Buy credits to keep using the API, and [contact us](https://sapling.ai/contact?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs) once your card is fixed to go back to monthly billing. ## Payment-required responses[​](#payment-required-responses "Direct link to Payment-required responses") Authenticated API requests can return `402 Payment Required` when the API key has expired, the associated subscription is inactive, the account has reached its monthly billing limit, or its prepaid credit balance is depleted. These responses include a `Retry-After: 3600` header, where the value is the number of seconds clients should wait before trying again. The header is listed in `Access-Control-Expose-Headers`, so cross-origin browser clients can read it. Because resolving a `402` usually requires renewing access or updating billing settings, clients should inspect the response's `msg` field and avoid retrying continuously while the account remains inactive. One `402` is temporary. When a free-trial account uses up its daily character allowance, the block clears at the next UTC midnight. That response's `Retry-After` header and a numeric `retry_after` body field both give the seconds until the reset. To check a key's state without making a billable call, including the exact reason it is blocked, use [Account Status](https://sapling.ai/docs/api/account-status.md). See [Rate Limits & Errors](https://sapling.ai/docs/rate_limits.md) for every status code. The response body is JSON with a `msg` field describing the reason: ``` { "msg": "API credit balance depleted. Purchase more credits at https://sapling.ai/api_settings#billing." } ``` <br /> Production Key Trial development keys allow for processing of 50,000 characters every 24 hours. [Subscribe](https://sapling.ai/api-checkout) for production access and [usage-based pricing](https://sapling.ai/docs/api/pricing), or purchase [prepaid API credits](#prepaid-api-credits) to activate production access without a metered subscription. <br /> # Replacing Grammarly with Sapling We cover both how to use Sapling through an HTML `script` tag, as well as how to bundle it with your JavaScript application with `npm`. Besides changing an import and a new mechanism for observing an editable, remember to tag editables with `sapling-ignore` to prevent users of the Sapling browser extension from getting duplicate UI elements. ## HTML / JavaScript[​](#html--javascript "Direct link to HTML / JavaScript") ### Old Grammarly Code[​](#old-grammarly-code "Direct link to Old Grammarly Code") ``` <script src="https://cdn.jsdelivr.net/npm/@grammarly/editor-sdk?clientId=YOUR_CLIENT_ID"></script> <grammarly-editor-plugin> <div id="editor" contenteditable="true"></div> </grammarly-editor-plugin> ``` ### New Sapling Code[​](#new-sapling-code "Direct link to New Sapling Code") ``` <script src="https://cdn.sapling.ai/sapling-sdk.js"></script> <div id="editor" sapling-ignore="true" contenteditable="true"></div> <script type="text/javascript"> Sapling.init({ key: '<api-key>', mode: 'dev', }); const contentEditable = document.getElementById('editor'); Sapling.observe(contentEditable); </script> ``` ## NPM[​](#npm "Direct link to NPM") ### Old Grammarly Code[​](#old-grammarly-code-1 "Direct link to Old Grammarly Code") Installation: `npm install @grammarly/editor-sdk` Editor initialization: ``` <grammarly-editor-plugin> <div id="editor" contenteditable="true"></div> </grammarly-editor-plugin> ``` JavaScript initialization (we recommend adding this after the editor HTML): ``` import * as Grammarly from "@grammarly/editor-sdk"; await Grammarly.init("YOUR_CLIENT_ID"); ``` ### New Sapling Code[​](#new-sapling-code-1 "Direct link to New Sapling Code") Installation: `npm install @saplingai/sapling-js` Editor initialization: ``` <div id="editor" sapling-ignore="true" contentEditable="true"> </div> ``` JavaScript initialization (we recommend adding this after the editor HTML): ``` Sapling.init({ key: '<YOUR_API_KEY>', mode: 'dev', }); const editor = document.getElementById('editor'); Sapling.observe(editor); ``` ## Additional Functionality[​](#additional-functionality "Direct link to Additional Functionality") ### Initialization Flags[​](#initialization-flags "Direct link to Initialization Flags") Sapling's SDK also provides additional functionality that is not available in the Grammarly SDK: * Support for other languages as well as language varieties (e.g., US or UK English). * Custom error mappings and custom dictionaries. * The ability to check text on a JavaScript callback instead of during textchange events to trigger checks manually or to reduce usage. * [Advanced Edits:](https://sapling.ai/docs/api/advanced-edits.md) corrections that go beyond regular spelling and grammar checking such as for DEI initiatives. * Customization of the the edit UI and color scheme through [style customization](<https://sapling.ai/docs/sdk/Integration Details/customize.md>). ### Backend API[​](#backend-api "Direct link to Backend API") Sapling provides a "headless" API that works without HTML UI elements as well. You can use this to power your backend applications. The `npm` JavaScript SDK supports this behavior natively, and we also provide a Python SDK. For other languages, you can call the HTTP endpoint with your API key. We include examples here: [https://sapling.ai/programming-languages](https://sapling.ai/programming-languages?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs). ## Next Steps[​](#next-steps "Direct link to Next Steps") * You can browse Sapling's SDK functionality and learn more about the platform through our [Developer Documentation](https://sapling.ai/docs/sdk/JavaScript/quickstart.md). * Remember to to remove the Grammarly SDK in your code as well as in your `package.json`. * If at any point you run into issues with implementation or testing, feel free to reach out to us at <support@sapling.ai> or our [Discord channel](https://discord.com/invite/p5gDc684TX). We also try to answer all questions tagged `sapling` on [Stackoverflow](https://stackoverflow.com/questions/tagged/sapling). --- # Salesforce Lightning info For technical support, contact <support@sapling.ai>. Sapling's SDK integrates with Salesforce Lightning applications, offering the simplest way to add grammar and spell checking functionality to a Lightning application. ## Lightning Web Components (LWC) / Locker Service[​](#lightning-web-components-lwc--locker-service "Direct link to Lightning Web Components (LWC) / Locker Service") Locker Service is a Salesforce-specific JavaScript runtime environment that does not behave completely like browser web APIs. Platform differences and Salesforce-specific JavaScript implementation details are noted here. ## Shadow DOM / Shadow Root[​](#shadow-dom--shadow-root "Direct link to Shadow DOM / Shadow Root") By default, Sapling's SDK renders markup in encapsulated ShadowRoot (Shadow DOM) elements to isolate them from the parent DOM's HTML, CSS, and JavaScript. Salesforce Lightning Web Components do not support shadow DOMs, so set the flag to `false` on instantiation. ``` Sapling.init({ ... shadowRoot: false, }); ``` Refer to: [Salesforce Shadow DOM Documentation](https://developer.salesforce.com/docs/component-library/documentation/en/lwc/lwc.create_dom) ## Textarea Parent Access[​](#textarea-parent-access "Direct link to Textarea Parent Access") When the Sapling SDK observes an HTML DOM element, it uses the element's non-static parent to position edit suggestions properly. The Lightning platform will sometimes prevent access to parent objects not owned by the Lightning Web Component. Without this, Chrome will render Sapling markup against the wrong non-static parent. A workaround is to wrap Sapling elements in a div with a non-static position: ``` <div style="position:relative;"> <textarea></textarea> </div> ``` The non-static element does not have to be immediate. If it's simpler, the root (accessible) element can be a non-static position. ## Component Element Access[​](#component-element-access "Direct link to Component Element Access") Lightning DOM elements are accessed through the `template` property instead of the document `property`. ``` const textareas = this.template.querySelectorAll('textarea'); textareas.forEach(ta => { Sapling.observe(ta); }); ``` Refer to: [Salesforce DOM Access Documentation](https://developer.salesforce.com/docs/component-library/documentation/en/lwc/create_components_dom_work) ## HTML Element Support[​](#html-element-support "Direct link to HTML Element Support") info We recommend using `textarea` elements instead with Sapling for best compatibility, instead of Lightning web elements such as `lightning-textarea` and `lightning-input-field`. Sapling can work with all standard web HTML text elements: `textarea`, `contenteditable`, `input`. The Lightning platform also includes additional web elements like `lightning-textarea` and `lightning-input-field`. These elements encapsulate standard web text elements like `textarea`. The DOM elements are proxied and underlying web elements cannot be accessed. Sapling has partial support for Lightning web elements like `lightning-textarea` and `lightning-input-field`. Issues with encapsulated Lightning components are listed below: #### Scroll Events[​](#scroll-events "Direct link to Scroll Events") Lightning text wrapper do not emit scroll events that other DOM scrollables normally emit. This means that Sapling markup on these components does not update when scrolled. A workaround is to disable scrolling in these elements and wrap them in a scrollable parent (`div`) that Sapling can access. Another workaround is to call `Sapling.checkOnce` on components manually when you believe the Sapling markup needs to be refreshed, either based on DOM events, a timer, or a manually triggered button. #### Positioning[​](#positioning "Direct link to Positioning") Lightning text wrappers do not allow inspection of the computed vertical and horizontal positioning of the underlying textarea that is being wrapped. Things that affect the positioning include padding, margin, and label sizes. If Sapling markup is positioned incorrectly, make sure that custom padding, margin, and label sizes apply to standard classes like `slds-form-element__label` and `slds-textarea`. A workaround is instantiating the Sapling SDK with horizontal and vertical pixel offsets to adjust for positioning issues. ``` Sapling.init({ ... lightningCompatTop: 10, lightningCompatLeft: 12, }); ``` --- # AI Detect JavaScript Quickstart <!-- --> Developer Key A rate-limited API developer key can be provisioned from the [Sapling API dashboard](https://sapling.ai/api_settings?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs). Developer keys allow for processing of 50,000 characters every 24 hours. [Subscribe](https://sapling.ai/api-checkout) for production access and [usage-based pricing](https://sapling.ai/docs/api/pricing). ## Javascript SDK for AI Detection[​](#javascript-sdk-for-ai-detection "Direct link to Javascript SDK for AI Detection") This page explains how Sapling's Javascript SDK provides a plug-and-play UI integration for AI content detection. Using this AI detection SDK allows developers to easily add the ability to flag AI-generated content in any textarea / contenteditable. If you would rather call the endpoint directly from your own backend, see the [AI detector API reference](https://sapling.ai/docs/api/detector.md). ### 1. Full-stack setup[​](#1-full-stack-setup "Direct link to 1. Full-stack setup") The AI Detect integration builds upon our **[Fullstack Javascript Quickstart](https://sapling.ai/docs/sdk/JavaScript/quickstart.md)**. Be sure to complete that guide before continuing. ### 2. Add AI Detection button[​](#2-add-ai-detection-button "Direct link to 2. Add AI Detection button") Open `App.js` and replace its contents with the following code /\<project-folder>/client/src/App.js ``` import './App.css'; import { useEffect } from 'react'; import { Sapling } from "@saplingai/sapling-js/observer"; function App() { useEffect(() => { Sapling.init({ endpointHostname: 'http://127.0.0.1:5000', saplingPathPrefix: '/sapling', }); const editor = document.getElementById('editor'); Sapling.observe(editor); const button = document.getElementById('ai-detect-btn'); button.addEventListener('click', () => { Sapling.checkOnce(editor, {aiDetect: true}); }); }); return ( <> <div id="editor" sapling-ignore="true" contentEditable="true" style={{ margin: '40px auto', padding: '10px', border: '2px solid black', width: '500px', }}> Phone chargers are essential accessories that have become an integral part of modern life. These devices are designed to replenish the battery life of our smartphones and other electronic gadgets, enabling us to stay connected and productive throughout the day. With advancements in technology, phone chargers have evolved significantly, offering various types such as wired, wireless, fast-charging, and portable chargers. <br/><br/> The convenience and versatility of phone chargers have revolutionized the way we use our devices, ensuring we can always stay powered up and connected in our fast-paced, digital world. Portable phone chargers, also known as power banks, have become a lifesaver for people on the go. These compact, battery-powered devices store electrical energy that can be used to charge our phones when an outlet is unavailable. Whether traveling, camping, or during emergencies, power banks ensure we are never left stranded with a dead phone battery. </div> <button id="ai-detect-btn" style={{ padding: '10px', border: '2px solid black', width: '500px', height: '40px', cursor: 'pointer', }}>Check AI</button> </> ); } export default App; ``` ### 3. Test it out\![​](#3-test-it-out "Direct link to 3. Test it out!") That's it! Go to **<http://localhost:3000>** and click the button! You'll see an overall score pop-up, and suspicious sentences higlighted in red. ![](/img/sdk/ai_detect_sdk_2.png) ### 4. One-time checks in a textarea[​](#4-one-time-checks-in-a-textarea "Direct link to 4. One-time checks in a textarea") With the browser SDK bundled in `@saplingai/sapling-js` 1.0.54 (SDK bundle v1.1.3), you can run a one-time AI detection check on a `<textarea>` without first calling `Sapling.observe()`. To try it, replace the contents of `App.js` from step 2 with this version, which renders a textarea and never observes it: /\<project-folder>/client/src/App.js ``` import './App.css'; import { useEffect } from 'react'; import { Sapling } from "@saplingai/sapling-js/observer"; function App() { useEffect(() => { Sapling.init({ endpointHostname: 'http://127.0.0.1:5000', saplingPathPrefix: '/sapling', }); }, []); const checkAI = () => { // No Sapling.observe() call: this is a one-time check. const textarea = document.getElementById('ai-detect-textarea'); Sapling.checkOnce(textarea, {aiDetect: true}); }; return ( <> <textarea id="ai-detect-textarea" rows={10} defaultValue="Paste text here to check whether it was written by AI." style={{ display: 'block', margin: '40px auto 0', padding: '10px', border: '2px solid black', width: '500px', }} /> <button id="ai-detect-btn" onClick={checkAI} style={{ display: 'block', margin: '0 auto', padding: '10px', border: '2px solid black', width: '500px', height: '40px', cursor: 'pointer', }}>Check AI</button> </> ); } export default App; ``` Keep the API key on your backend as in the full-stack setup above. `Sapling.observe()` remains appropriate for continuous grammar suggestions. AI-detection sentence highlights and score badges also stay aligned with the text when a textarea or scrollable contenteditable is scrolled. --- # Backend Documentation Import the Sapling client with: ``` import { Client } from "@saplingai/sapling-js/client"; ``` ## constructor(...)[​](#constructor "Direct link to constructor(...)") Returns a Sapling HTTP API Client wrapper. ### Request Parameters[​](#request-parameters "Direct link to Request Parameters") **apiKey**: String<br />32-character API key **options**: Optional Object<br /> Supported option fields: * **timeout**: Optional Integer <br /> Timeout for API call in seconds. Defaults to 120000 milliseconds. * **hostname**: Optional String <br /> Hostname override for SDK and self-hosted deployments. * **pathname**: Optional String <br /> Pathname override for SDK and self-hosted deployments as well as version requirements. ## Custom mappings and custom filters[​](#custom-mappings-and-custom-filters "Direct link to Custom mappings and custom filters") The server-side JavaScript client provides methods to manage the custom corrections and filters attached to an API key. Keep the API key in your backend, rather than embedding it in browser JavaScript. ``` import { Client } from "@saplingai/sapling-js/client"; const client = new Client(process.env.SAPLING_API_KEY); // Suggest this capitalization whenever "sapling" is encountered. const { data: rule } = await client.addCustomMapping("sapling", "Sapling", { description: "Product name capitalization", case_sensitive: true, }); const { data: mappings } = await client.customMappings(); // mappings.custom_mappings contains the account's mapping rules. await client.deleteCustomMapping(rule.id); // HTTP 204; empty body // Suppress a particular correction (rather than forcing a replacement). const { data: filter } = await client.addCustomFilter( "Patient", "The patient", { case_sensitive: true } ); const { data: filters } = await client.customFilters(); // filters.custom_filters contains the account's filter rules. await client.deleteCustomFilter(filter.id); // HTTP 204; empty body ``` | Method | Description | | -------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | | `customMappings()` | List mapping rules for the account. | | `addCustomMapping(entry, mapping, options?)` | Create a correction with optional `description` and `case_sensitive` (default true). | | `deleteCustomMapping(id)` | Delete a mapping by its UUID. | | `customFilters()` | List filters that suppress edits. | | `addCustomFilter(input, output, options?)` | Create a filter with optional `case_sensitive`; either side can be `""` to act as a wildcard, but not both. | | `deleteCustomFilter(id)` | Delete a filter by its UUID. | These methods wrap the [Custom Mappings and Custom Filters API](https://sapling.ai/docs/api/custom-mappings.md). Mappings support sentence-boundary markers (`^`, `$`) and `#` number placeholders. A mapping's description is displayed alongside its suggestion. The API can return HTTP `400` for duplicate, invalid, or unauthorized rule operations; deleting a rule returns an empty HTTP `204` response. All six operations are unbilled. ## Usage and quota reporting[​](#usage-and-quota-reporting "Direct link to Usage and quota reporting") The server-side JavaScript client in `@saplingai/sapling-js` 1.0.54 and later provides four unbilled reporting methods. Use a private API key on your server. All four accept an optional options object with `start_days_back` and `end_days_back`: non-negative integer offsets from today. Omitted values use each endpoint's own defaults. ``` import { Client } from "@saplingai/sapling-js/client"; const client = new Client(process.env.SAPLING_API_KEY); const { data: usage } = await client.apiUsage({ start_days_back: 30, end_days_back: 0, }); console.log(usage.total_characters, usage.total_cost_usd, usage.by_endpoint); const { data: endpoints } = await client.apiEndpointUsage({ start_days_back: 7, end_days_back: 0, interval: "Daily", }); console.log(endpoints.data); const { data: quota } = await client.apiQuotaUsage({ start_days_back: 7, end_days_back: 0, }); console.log(quota.quota, quota.usage); const { data: activity } = await client.apiUserActivity({ start_days_back: 7, end_days_back: 0, }); console.log(activity.edits_shown, activity.active_users); ``` | Method | Returns | | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `apiUsage(options?)` | Total characters and list-price cost, broken down by masked API key, endpoint, day, and month. Also includes `credit_balance_data` for non-team-scoped keys. | | `apiEndpointUsage(options?)` | Per-endpoint characters in `data`, bucketed by `interval: "Daily"` (default), `"Weekly"`, or `"Monthly"`. | | `apiQuotaUsage(options?)` | Monthly `quota` (`-1` means unlimited), window `usage`, and current-month `accepts` / `ignores` counts. | | `apiUserActivity(options?)` | `edits_shown`, `edits_accepted`, `edits_ignored`, and `active_users` counts. | The client validates window offsets and the interval before sending the request. The API rejects invalid reporting windows, and windows over one year return HTTP `403`. For `apiUsage()`, a team-scoped key reports team usage; a user key reports usage for that user. API keys are masked in results. See the [API Reporting reference](https://sapling.ai/docs/api/api-reporting.md) for the underlying endpoints, defaults, and complete response fields. ## edits(...)[​](#edits "Direct link to edits(...)") ### Request Parameters[​](#request-parameters-1 "Direct link to Request Parameters") **text**: String<br />Text to process for edits. **sessionId**: Optional String<br />Unique name or UUID of document or portion of text that is being checked **options**: Optional Object<br /> Supported options fields: * **lang**: Optional String <br /> Specifies language for grammar and spell checking. Supported Languages Specifies language for grammar and spell checking. Defaults to English, or the language specified in the dashboard. Supported languages: * `auto`: Autodetect language * `de`: German (Deutsch) * `el`: Greek (Ελληνικά) * `en`: English (US/UK/CA/AU) * `es`: Spanish (Español) * `fr`: French (Français) (`fr-fr` and `fr-ca` coming soon) * `it`: Italian (Italiano) * `jp`: Japanese (日本語) * `ko`: Korean (한국어) * `nl`: Dutch (Nederlands) * `pl`: Polish (Polski) * `pt`: Portuguese (Português) (`pt-pt` and `pt-br` coming soon) * `sv`: Swedish (Svenska) * `tl`: Tagalog * `zh`: Chinese (中文) - **variety**: Optional String Supported Varieties This option is currently available for: English, Chinese (中文). Specifies regional English/Chinese spelling options. The setting defaults to the configuration in the Sapling dashboard. Supported varieties: English: * `us-variety`: American English * `gb-variety`: British English * `au-variety`: Australian English * `ca-variety`: Canadian English * `null-variety`: Don’t suggest any changes based on English variety Chinese (中文) * `s-variety`: Simplified Chinese * `t-variety`: Traditional Chinese - **auto\_apply**: Optional Boolean<br />Default is false. If true, will return field containing text with edits applied. Note: this argument does not support HTML. - **medical**: Optional Boolean<br />Default is false. If true, the backend will apply Sapling's medical dictionary. You can test the medical spellcheck system [here](https://sapling.ai/utilities/medical-spellcheck?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs). - **advancedEdits**: Optional Object<br /> Supported advancedEdits fields: * advanced\_edits * adverbs * simplifications * hard\_to\_read * qualifiers * voice * dei * gender * gender\_pronoun * gender\_noun * gender\_id * sensitivity * disability * age * race * social\_class * violence ### Response Parameters[​](#response-parameters "Direct link to Response Parameters") Promise, returns JSON with parameters documented below: **edits**: Array<br />Array of `Edit` objects for the text sent in the request. Edit Data Structure: ``` { "id": <str, UUID>, // Opaque edit id, used to give feedback "sentence": <str>, // Unedited sentence "sentence_start": <int>, // Offset of sentence from start of text "start": <int>, // Offset of edit start relative to sentence "end": <int>, // Offset of edit end relative to sentence "replacement": <str>, // Suggested replacement "error_type": <str>, // Error type, see "Error Categories" "general_error_type": <str>, // See "Error Categories" } ``` ## accept(...)[​](#accept "Direct link to accept(...)") Use this method to have Sapling adapt its system over time. Each suggested edit has an edit UUID. You can pass this information back to Sapling to indicate the edit suggestion was helpful. For each unique edit in each document, use the accept or reject method only once in total. ### Request Parameters[​](#request-parameters-2 "Direct link to Request Parameters") **editUUID**:String (UUID)<br />ID of an `Edit`. **sessionId**: String<br />Unique name or UUID of article or portion of text that is being checked ### Response Parameters[​](#response-parameters-1 "Direct link to Response Parameters") `null` ## reject(...)[​](#reject "Direct link to reject(...)") Use this method to have Sapling not recommend the same edit anymore. Each suggested edit has an edit UUID. You can pass this information back to Sapling to indicate the edit suggestion was not helpful. For each unique edit in each document, use the accept or reject method only once in total. ### Request Parameters[​](#request-parameters-3 "Direct link to Request Parameters") **editUUID**: String (UUID)<br />ID of an `Edit`. **sessionId**: String<br />Unique name or UUID of article or portion of text that is being checked ### Response Parameters[​](#response-parameters-2 "Direct link to Response Parameters") `null` --- # Frontend Documentation The Sapling JavaScript SDK enables easy integration of Sapling's functionality -- such as grammar/spell checking and autocomplete -- in any web application. [SDK Demo](https://sapling.ai/sdk-demo) ## Sapling.init[​](#saplinginit "Direct link to Sapling.init") ``` Sapling.init({ endpointHostname: 'https://localhost:3000', lang: 'en', mode: 'prod' }); ``` `init` instantiates the SDK. **key** - String Sapling API key. DO NOT set this in JavaScript except for internal development. **mode** - String One of `dev` (during which the API key can be set inline) or `prod` (where API key should be set in backend). Refer to the [JavaScript quickstart](https://sapling.ai/docs/sdk/JavaScript/quickstart.md) for a more detailed explanation of this parameter. **userId** - Optional String This allows you to see accurate counts of your end users on Sapling dashboards. If not passed in, Sapling will assign an anonymized ID for this browser. **isAnonUser** - Optional Boolean If you wish to explicitly assign your own anonymous IDs for logged out users, you can set this argument to true. By default, this will be set to false if you're passing in a userId. **endpointHostname** - String Use your own backend hostname like the example in [Quickstart](https://sapling.ai/docs/sdk/JavaScript/quickstart.md). **saplingPathPrefix** - String If your backend handles other routes, you can prefix the SDK endpoints with a path. For example, for the following `init` call, ``` Sapling.init({ endpointHostname: 'http://127.0.0.1:5000', saplingPathPrefix: '/sapling', }); ``` edit requests will be routed to `http://127.0.0.1:5000/sapling/api/v1/edits` **editPathname** - String Use your own backend endpoint pathname like the example in [Quickstart](https://sapling.ai/docs/sdk/JavaScript/quickstart.md). Note that this does not have to be a full edits endpoint; you can also use a Sapling spellcheck endpoint to only display spellcheck edits. **statusBadge** - Boolean When set to true, will display a small blue status dot in the bottom right corner of observed text fields. The badge will spin to indicate that the text is being processed. We recommend turning this on for a better user experience, except when there are many (such as 10 or more) text fields on a single page and that many badges might be distracting. See additional information for [style customization here.](<https://sapling.ai/docs/sdk/Integration Details/customize.md>) **lang** - String Supported Languages Specifies language for grammar and spell checking. Defaults to English, or the language specified in the dashboard. Supported languages: * `auto`: Autodetect language * `de`: German (Deutsch) * `el`: Greek (Ελληνικά) * `en`: English (US/UK/CA/AU) * `es`: Spanish (Español) * `fr`: French (Français) (`fr-fr` and `fr-ca` coming soon) * `it`: Italian (Italiano) * `jp`: Japanese (日本語) * `ko`: Korean (한국어) * `nl`: Dutch (Nederlands) * `pl`: Polish (Polski) * `pt`: Portuguese (Português) (`pt-pt` and `pt-br` coming soon) * `sv`: Swedish (Svenska) * `tl`: Tagalog * `zh`: Chinese (中文) **variety** - String Supported Varieties This option is currently available for: English, Chinese (中文). Specifies regional English/Chinese spelling options. The setting defaults to the configuration in the Sapling dashboard. Supported varieties: English: * `us-variety`: American English * `gb-variety`: British English * `au-variety`: Australian English * `ca-variety`: Canadian English * `null-variety`: Don’t suggest any changes based on English variety Chinese (中文) * `s-variety`: Simplified Chinese * `t-variety`: Traditional Chinese **neuralSpellcheck** - Optional Boolean Default is false. If true, system will also apply more aggressive spellcheck to catch more spelling errors than the base system alone. **minLength** - Optional Integer Sets `min_length` parameter when using SDK with [Spellcheck](https://sapling.ai/docs/api/spellcheck.md) API. **operations** - Optional List of Strings Default is `[]`. Possible operations are described in the [Postprocess](https://sapling.ai/docs/api/postprocess.md) documentation. This will run postprocessing on outputs before returning results so you can enforce stricter capitalization, punctuation, and whitespace removal. **medical** - Optional Boolean Default is false. If true, the backend will apply Sapling's medical dictionary. You can test the medical spellcheck system [here](https://sapling.ai/utilities/medical-spellcheck?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs). **autocomplete** - Boolean When set to true, will use Sapling's autocomplete language model to suggest sentence autocompletions as a user is typing. **ignoreText** - String String to use in place of `Ignore` in Sapling's inline edit suggestions. **headers** - Dictionary Use this argument to specify custom headers for calls to your own backend. The dictionary should consist of key-value mappings (`{"key": "value"}`). * `key`: request header name (e.g., `Authorization`) * `value`: header value (e.g., `Bearer xxxxxxxxxxx`) **advancedEdits** - Dictionary Examples of advanced edits can be found [here](https://sapling.ai/docs/api/advanced-edits.md) Advanced Edit Options Options for advanced edits. Option keys are `String`, and option values are `Boolean`. * `advanced_edits`: Group setting, covering all the options below * `adverbs` * `simplifications` * `hard_to_read` * `qualifiers` * `voice` * `dei`: Group setting, covering all DEI options below * `gender`: Group setting, covering `gender_pronoun`, `gender_noun`, and `gender_id` * `gender_pronoun` * `gender_noun` * `gender_id` * `sensitivity` * `disability` * `age` * `race` * `social_class` * `violence` A group setting marked `false` will disable all categories it covers. **autoShowAnnotations** - Boolean When set to true, will show advanced edits by default. Otherwise, the edits will only appear after toggling them on from the status badge. ## Sapling.observe[​](#saplingobserve "Direct link to Sapling.observe") ``` const editable = document.getElementById('editor'); Sapling.observe(editable); ``` `observe` takes a single argument, a textarea or contenteditable. The SDK will check text in the dom element in real time and apply edit suggestions. Mark elements that will be observed by the JavaScript SDK with the attribute `sapling-ignore="true"` to prevent duplicate interactions with the Sapling browser extension. Mark elements inside a contenteditable with `data-sapling-ignore="true"` to prevent the dom node from being processed by Sapling. ## Sapling.unobserve[​](#saplingunobserve "Direct link to Sapling.unobserve") ``` const editable = document.getElementById('editor'); Sapling.unobserve(editable); ``` `unobserve` takes a single argument, a textarea or contenteditable. This will remove edit suggestions and stop any checking on a previously observed element. ## Sapling.checkOnce[​](#saplingcheckonce "Direct link to Sapling.checkOnce") ``` const editable = document.getElementById('editor'); Sapling.checkOnce(editable); ``` `checkOnce` takes a single argument, a textarea or contenteditable. This function is only recommended for internal use. Sentences that have not been changed are cached locally and do not count towards bandwidth and API usage more than once. However, SDK users may choose not to trigger text checks in real time for additional bandwidth or API usage savings. note Edit markup is applied to the text using DOM Range objects. While they typically update in location with text, depending on changes in text, individual edits may no longer be accurate after the text has been changed, and may result in a confusing user experience. ## Deploying to Production[​](#deploying-to-production "Direct link to Deploying to Production") There are several methods of deploying Sapling's SDK without exposing your private API Key. These can be found [here](https://sapling.ai/docs/sdk/JavaScript/frontend_key_management.md): 1. [Proxying requests through your backend](https://sapling.ai/docs/sdk/JavaScript/frontend_key_management.md#1-proxying-requests) 2. [Authentication with a JWT key](https://sapling.ai/docs/sdk/JavaScript/frontend_key_management.md#2-jwt-keys) --- # Frontend Key Management There are two main ways of authenticating SDK server requests without exposing your API key in public environments: 1. [Proxying requests through your backend](#1-proxying-requests) 2. [Authentication with a JWT key](#2-jwt-keys) ## 1. Proxying Requests[​](#1-proxying-requests "Direct link to 1. Proxying Requests") This is a way to use the SDK by adding the private API Key to the API calls from a backend server. * Set up a backend endpoint with your API key to request edits. * Text from the public page is passed to the backend which adds the API key before proxying onto Sapling servers. ![](/img/sdk/api_key_diagram.png) A full working example using React, Webpack and Express can be downloaded here: * <https://github.com/saplingai/sapling-samples/tree/master/javascript/express-app> #### Frontend Code (javascript/express-app/src/App.js)[​](#frontend-code-javascriptexpress-appsrcappjs "Direct link to Frontend Code (javascript/express-app/src/App.js)") ``` Sapling.init({ endpointHostname: 'http://127.0.0.1:3000', // Set endpoint to your own server saplingPathPrefix: '/sapling', }); ``` #### Backend Code (javascript/express-app/src/server.mjs)[​](#backend-code-javascriptexpress-appsrcservermjs "Direct link to Backend Code (javascript/express-app/src/server.mjs)") ``` const SAPLING_API_URL = 'https://api.sapling.ai'; const API_KEY = '<private-api-key>'; app.post('/sapling/*', (req, res, next) => { // remove the '/sapling/' prefix from the request path let requestPath = req.path.substring(8); // pass request path along to Sapling let requestUrl = `${SAPLING_API_URL}${requestPath}`; // add the API Key req.body.key = API_KEY; axios({ url: requestUrl, data: req.body, method: 'post', }) .then(function (response) { res.statusCode = 200; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(response.data)); }); }) ``` ## 2. JWT Keys[​](#2-jwt-keys "Direct link to 2. JWT Keys") This is a way to generate a short term JWT token for authentication without exposing your private API Key. You can sign a JWT token with the following claims: ``` { "sub": "<public-api-key>", "exp": <expiration_time>, } ``` The expiration time is specified as seconds from Epoch (January 1, 1970 Midnight UTC). The generated JWT token can be passed into the SDK under the `key` field without exposing the private key. Here are some examples of generating JWT tokens: * JavaScript * Python * PHP * Java ``` const jwt = require('jsonwebtoken'); const current_time = Math.floor(Date.now() / 1000); const expiration_time = current_time + 864000; // 10 days const private_key = '<private-api-key>'; const public_key = '<public-api-key>'; const claims = { 'sub': public_key, 'exp': expiration_time, }; const jwtToken = jwt.sign(claims, private_key, { algorithm: 'HS256' }); console.log(jwtToken); ``` ``` import time import jwt current_time = int(time.time()) expiration_time = current_time + 864000 # 10 days private_key = '<private-api-key>' public_key = '<public-api-key>' claims = { 'sub': public_key, 'exp': expiration_time, } jwt_token = jwt.encode(claims, private_key, algorithm='HS256') print(jwt_token) ``` ``` <?php require 'vendor/autoload.php'; use Firebase\JWT\JWT; $current_time = time(); $expiration_time = $current_time + 864000; // 10 days $private_key = '<private-api-key>'; $public_key = '<public-api-key>'; $claims = [ 'sub' => $public_key, 'exp' => $expiration_time, ]; $jwt_token = JWT::encode($claims, $private_key, 'HS256'); echo $jwt_token; ?> ``` ``` import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; import java.util.Date; public class JwtEncode { public static void main(String[] args) { long currentTimeMillis = System.currentTimeMillis(); long expirationMillis = currentTimeMillis + 864000000L; // 10 days in milliseconds String privateKey = "<private-api-key>"; String publicKey = "<public-api-key>"; Algorithm algorithm = Algorithm.HMAC256(privateKey); String jwtToken = JWT.create() .withSubject(publicKey) .withExpiresAt(new Date(expirationMillis)) .sign(algorithm); System.out.println(jwtToken); } } ``` --- # JavaScript Quickstart <!-- --> Developer Key A rate-limited API developer key can be provisioned from the [Sapling API dashboard](https://sapling.ai/api_settings?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs). Developer keys allow for processing of 50,000 characters every 24 hours. [Subscribe](https://sapling.ai/api-checkout) for production access and [usage-based pricing](https://sapling.ai/docs/api/pricing). ## Installation[​](#installation "Direct link to Installation") Install the `@saplingai/sapling-js` package with [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) ``` npm install @saplingai/sapling-js ``` or [yarn](https://yarnpkg.com/): ``` yarn add @saplingai/sapling-js ``` ## Fullstack Quickstart[​](#fullstack-quickstart "Direct link to Fullstack Quickstart") Here's an example of a basic React App using the Sapling JavaScript observer on the frontend and Sapling backend client for securely managing your API key. ### 1. Initialize a NodeJS React app[​](#1-initialize-a-nodejs-react-app "Direct link to 1. Initialize a NodeJS React app") Run these commands to create a basic React.js app scaffolding and install the necessary npm packages. ``` cd <project-folder> npm init -y npm install express axios cors --save sudo npm install -g create-react-app npx create-react-app client cd client && npm install @saplingai/sapling-js --save cd .. ``` ### 2. Setup server-side code[​](#2-setup-server-side-code "Direct link to 2. Setup server-side code") Create a file outside the `client` folder called **server.js** and paste in the following code. This will set up a basic server that will render your React app. /\<project-folder>/server.js ``` const express = require('express'); const path = require('path'); const cors = require('cors'); const axios = require('axios'); const app = express(); app.use(cors()) app.use(express.json()); app.use(express.static(path.join(__dirname, 'client/build'))); app.get('*', (req, res) => { res.sendFile(path.join(__dirname + '/client/build/index.html')); }); // PLACEHOLDER FOR SAPLING ENDPOINT // You can change default port but make sure to change for frontend `endpointHostname` as well const port = process.env.PORT || 5000; app.listen(port); console.log('App is listening on port ' + port); ``` ### 3. Run your app[​](#3-run-your-app "Direct link to 3. Run your app") In one terminal, run the following code to run the React client code. This will also automatically watch App.js for changes. ``` cd <project-folder>/client npm run start ``` Open a new terminal, ``` cd <project-folder> node server.js ``` The app should now be up and running - open **<http://localhost:3000>** in your browser. ### 4. Set up a Sapling observer[​](#4-set-up-a-sapling-observer "Direct link to 4. Set up a Sapling observer") Open `App.js` and replace its contents with the following code /\<project-folder>/client/src/App.js ``` import './App.css'; import { useEffect } from 'react'; import { Sapling } from "@saplingai/sapling-js/observer"; function App() { useEffect(() => { Sapling.init({ endpointHostname: 'http://127.0.0.1:5000', saplingPathPrefix: '/sapling', }); const editor = document.getElementById('editor'); Sapling.observe(editor); }); return ( <div id="editor" sapling-ignore="true" contentEditable="true" style={{ margin: '40px auto', padding: '10px', border: '2px solid black', width: '500px', height: '200px' }}> Lets get started! </div> ); } export default App; ``` ### 5. Set up /sapling endpoint[​](#5-set-up-sapling-endpoint "Direct link to 5. Set up /sapling endpoint") Open `server.js` and replace the `// PLACEHOLDER FOR SAPLING ENDPOINT` with the following endpoint code /\<project-folder>/server.js ``` const SAPLING_API_URL = 'https://api.sapling.ai'; const API_KEY = '<api-key>'; app.post('/sapling/*', (req, res, next) => { // remove the '/sapling/' prefix from the request path let requestPath = req.path.substring(8); // pass request path along to Sapling let requestUrl = `${SAPLING_API_URL}${requestPath}`; // add the API Key req.body.key = API_KEY; axios({ url: requestUrl, data: req.body, method: 'post', }) .then(function (response) { res.statusCode = 200; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(response.data)); }); }) ``` caution Remember to replace `your-api-key` with the API key from **[https://sapling.ai/api\_settings](https://sapling.ai/api_settings?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs)** Restart the server ``` cd <project-folder> node server.js ``` ### 6. Test it out\![​](#6-test-it-out "Direct link to 6. Test it out!") That's it! Go to **<http://localhost:3000>** and you'll notice a grammatical mistake already highlighted. Simply hover over it and accept the change! ## Frontend-Only Quickstart[​](#frontend-only-quickstart "Direct link to Frontend-Only Quickstart") If you only need a frontend and will be using your own backend, here's an example of a minimal React App using the Sapling JavaScript observer. caution Do not serve a page like this publicly, as it exposes the API key. * During development, you can pass in the API key and use Sapling's endpoints by setting `mode: 'dev'` in the `init` function. * However, for production, do not pass the API key in JavaScript. Instead, include the API key in your backend and call Sapling's endpoints from the backend. This is the default setting with `mode: 'prod'`. ``` import { useEffect } from 'react'; import { Sapling } from "@saplingai/sapling-js/observer"; function App() { useEffect(() => { Sapling.init({ key: '<api-key>', endpointHostname: 'https://api.sapling.ai', editPathname: '/api/v1/edits', statusBadge: true, mode: 'dev', }); const editor = document.getElementById('editor'); Sapling.observe(editor); }); return ( <div id="editor" sapling-ignore="true" contentEditable="true"> Lets get started! </div> ); } ``` ## Backend-Only Quickstart[​](#backend-only-quickstart "Direct link to Backend-Only Quickstart") If you already have a frontend and will only be running a Node backend, here's an example of a minimal Node script using the Sapling JavaScript client. ``` import { Client } from "@saplingai/sapling-js/client"; const apiKey = '<api-key>'; const client = new Client(apiKey); client .edits('Lets get started!') .then(function (response) { console.log(response.data); }) ``` --- # Tone Detection JavaScript Quickstart <!-- --> Developer Key A rate-limited API developer key can be provisioned from the [Sapling API dashboard](https://sapling.ai/api_settings?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs). Developer keys allow for processing of 50,000 characters every 24 hours. [Subscribe](https://sapling.ai/api-checkout) for production access and [usage-based pricing](https://sapling.ai/docs/api/pricing). ## Javascript SDK for Tone Detection[​](#javascript-sdk-for-tone-detection "Direct link to Javascript SDK for Tone Detection") This page explains how Sapling's Javascript SDK provides a plug-and-play UI integration for Tone detection. Using tone detection SDK allows developers to easily add the ability to highlight the tone of content in any textarea / contenteditable, and flag any aggressive or negative tones. ### 1. Full-stack setup[​](#1-full-stack-setup "Direct link to 1. Full-stack setup") The Tone detection integration builds upon our **[Fullstack Javascript Quickstart](https://sapling.ai/docs/sdk/JavaScript/quickstart.md)**. Be sure to complete that guide before continuing. ### 2. Enable Tone[​](#2-enable-tone "Direct link to 2. Enable Tone") Open `App.js` and replace its contents with the following code /\<project-folder>/client/src/App.js ``` import './App.css'; import { useEffect } from 'react'; import { Sapling } from "@saplingai/sapling-js/observer"; function App() { useEffect(() => { Sapling.init({ endpointHostname: 'http://127.0.0.1:5000', saplingPathPrefix: '/sapling', toneEnabled: true, }); const editor = document.getElementById('editor'); Sapling.observe(editor); }); return ( <div id="editor" sapling-ignore="true" contentEditable="true" style={{ margin: '40px auto', padding: '10px', border: '2px solid black', width: '500px', height: '200px' }}> I'm really stoked about this! Will it be ready by next week? </div> ); } export default App; ``` ### 3. Test it out\![​](#3-test-it-out "Direct link to 3. Test it out!") That's it! Go to **<http://localhost:3000>** and click the button! You'll see an overall score pop-up, and suspicious sentences higlighted in red. ![](/img/sdk/tone_sdk.png) --- # Overview Sapling has SDKs that assist in grammar checking text for Python and JavaScript, and an HTTP API that is language agnostic. ## API Key[​](#api-key "Direct link to API Key") To use a Sapling SDK or the HTTP API, a private key needs to be provisioned first. Developer Key A rate-limited API developer key can be provisioned from the [Sapling API dashboard](https://sapling.ai/api_settings?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs). Developer keys allow for processing of 50,000 characters every 24 hours. [Subscribe](https://sapling.ai/api-checkout) for production access and [usage-based pricing](https://sapling.ai/docs/api/pricing). ## HTML[​](#html "Direct link to HTML") Here's the [HTML quickstart](https://sapling.ai/docs/sdk/HTML/quickstart.md). This is a JavaScript library that you can link to or serve locally. It has a Sapling observer that wraps `textarea` and `contenteditable` HTML elements and displays underlines and edits. ## JavaScript (Node.js)[​](#javascript-nodejs "Direct link to JavaScript (Node.js)") Here's the [JavaScript quickstart](https://sapling.ai/docs/sdk/JavaScript/quickstart.md). Currently Node.js is supported. Contact us for additional options. ### JavaScript Frontend[​](#javascript-frontend "Direct link to JavaScript Frontend") Here's the [JavaScript frontend documentation](https://sapling.ai/docs/sdk/JavaScript/frontend_documentation.md). This is an npm package that provides a Sapling observer that wraps `textarea` and `contenteditable` HTML elements and displays underlines and edits. This can be directly imported into JavaScript or files that transpile into JavaScript (such as Typescript/JSX). It can then be used with Node.js rendering frameworks such as [React](https://github.com/saplingai/sapling-samples/tree/master/javascript/react-app), Vue, and [Angular](https://github.com/saplingai/sapling-samples/tree/master/javascript/angular-app). ### JavaScript Backend[​](#javascript-backend "Direct link to JavaScript Backend") Here's the [JavaScript backend documentation](https://sapling.ai/docs/sdk/JavaScript/backend_documentation.md). Sapling provides an [ES6 (ECMAScript 2015)](https://nodejs.org/api/esm.html) npm package. The package wraps the Sapling HTTP API. ## Python[​](#python "Direct link to Python") Here's the [Python quickstart](https://sapling.ai/docs/sdk/Python/quickstart.md). Sapling provides a Python 3 wrapper for the Sapling HTTP API. This can be installed from [PyPI](https://pypi.org/project/sapling-py/) with pip or uv, and its source is available on [GitHub](https://github.com/saplingai/sapling-py). --- # Python Documentation The `sapling-py` package provides a Python wrapper around the Sapling HTTP API. Install or upgrade to the latest release with pip: ``` python -m pip install --upgrade sapling-py ``` Or add it to a uv project: ``` uv add sapling-py ``` The core methods below require `sapling-py` 4.1.0 or newer. Additional methods and batch examples on this page require version 4.11.0 or newer. ## Language API methods[​](#language-api-methods "Direct link to Language API methods") ``` from sapling import SaplingClient client = SaplingClient(api_key="<api-key>") rephrases = client.rephrase( "This is an example sentence.", mapping="paraphrase", num_results=3, ) summary = client.summarize("A longer document to summarize...") tones = client.tone("Thank you for resolving this so quickly!") sentiment = client.sentiment("The support experience was excellent.") quality = client.quality("Text to score for writing quality.") language = client.langdetect("Bonjour tout le monde.") profanity = client.profanity("Text to check.") ``` The client methods correspond to these API endpoints: | Client method | API documentation | | -------------- | ---------------------------------------------------------------- | | `rephrase()` | [Rephrase](https://sapling.ai/docs/api/rephrase.md) | | `summarize()` | [Summarize](https://sapling.ai/docs/api/summarize.md) | | `tone()` | [Tone](https://sapling.ai/docs/api/tone.md) | | `sentiment()` | [Sentiment](https://sapling.ai/docs/api/sentiment.md) | | `quality()` | [Quality Score](https://sapling.ai/docs/api/quality.md) | | `langdetect()` | [Language Detection](https://sapling.ai/docs/api/langdetect.md) | | `profanity()` | [Profanity Detection](https://sapling.ai/docs/api/profanity.md) | See each endpoint page for input limits, optional parameters, and response formats. ## Additional client methods (4.11.0+)[​](#additional-client-methods-4110 "Direct link to Additional client methods (4.11.0+)") The Python client also wraps classification, structured extraction, translation, content safety, AI detection, simplification, PII detection, and SEO analysis. Install `sapling-py` 4.11.0 or newer to use these methods. ``` from sapling import SaplingClient client = SaplingClient(api_key="<api-key>") classification = client.classify( "I was charged twice.", labels=["billing", "technical", "other"] ) extraction = client.extract( "Invoice INV-1042 is due March 5, 2026.", fields=["invoice_number", {"name": "due_date", "type": "date"}], ) translation = client.translate("Hello!", target_lang="fr") safety = client.safety("Text to assess.", spans=True) simplified = client.simplify( "The party shall remit payment upon receipt of the invoice.", reading_level="plain", preserve_terms=["invoice"], ) detector = client.aidetect("Text to check for AI-generated content.") ``` | Client method | API reference | | ------------- | ---------------------------------------------------------- | | `classify()` | [Classification](https://sapling.ai/docs/api/classify.md) | | `extract()` | [Extraction](https://sapling.ai/docs/api/extract.md) | | `translate()` | [Translation](https://sapling.ai/docs/api/translate.md) | | `safety()` | [Content Safety](https://sapling.ai/docs/api/safety.md) | | `simplify()` | [Simplification](https://sapling.ai/docs/api/simplify.md) | | `aidetect()` | [AI Detector](https://sapling.ai/docs/api/detector.md) | | `pii()` | [PII Detection](https://sapling.ai/docs/api/pii.md) | | `seo()` | [SEO Analysis](https://sapling.ai/docs/api/seo.md) | ### Batch requests[​](#batch-requests "Direct link to Batch requests") In version 4.11.0, `classify()`, `extract()`, `safety()`, `langdetect()`, `translate()`, `summarize()`, `quality()`, and `aidetect()` accept a list or tuple of **1–10 strings** instead of a single string. The client sends the API's `texts` parameter; options such as labels or target language apply to all items. The result contains a `results` array in input order, with one single-input response per item. Each endpoint's combined character limit still applies. ``` languages = client.langdetect(["Bonjour le monde.", "Hello world."]) print([item["lang"] for item in languages["results"]]) translations = client.translate( ["Hello!", "Thank you."], target_lang="fr" ) print([item["translation"] for item in translations["results"]]) ``` Other methods, including `simplify()`, take one text string. Consult the individual endpoint references for input and billing limits. ## Error handling[​](#error-handling "Direct link to Error handling") `sapling-py` raises `SaplingError` when the API returns a non-successful HTTP status. The exception exposes both the status code and raw response body: ``` from sapling import SaplingClient, SaplingError client = SaplingClient(api_key="<api-key>") try: result = client.summarize("Text to summarize.") except SaplingError as error: print(error.status_code) print(error.body) ``` Network and timeout failures continue to use the exceptions raised by the underlying `requests` library. Complete `SaplingClient` reference documentation is available on [Read the Docs](https://sapling.readthedocs.io/en/latest/api.html). --- # Python Quickstart Developer Key A rate-limited API developer key can be provisioned from the [Sapling API dashboard](https://sapling.ai/api_settings?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs). Developer keys allow for processing of 50,000 characters every 24 hours. [Subscribe](https://sapling.ai/api-checkout) for production access and [usage-based pricing](https://sapling.ai/docs/api/pricing). ## Installation[​](#installation "Direct link to Installation") Install the `sapling-py` package from PyPI with [pip](https://pip.pypa.io/en/stable/installation/): ``` python -m pip install sapling-py ``` Or add it to a [uv](https://docs.astral.sh/uv/) project: ``` uv add sapling-py ``` ## Getting Edits[​](#getting-edits "Direct link to Getting Edits") Here's a sample script using the Sapling package: ``` from sapling import SaplingClient api_key ='<api-key>' client = SaplingClient(api_key=api_key) edits = client.edits('Lets get started!', session_id='test_session') ``` The result of running the script should be an array of edits of this form: ``` [{ "id": "aa5ee291-a073-5146-8ebc-c9c899d01278", "sentence": "Lets get started!", "sentence_start": 0, "start": 0, "end": 4, "replacement": "Let's", "error_type": "R:OTHER", "general_error_type": "Other", }] ``` # Applying Edits After you've sent the Sapling API text and gotten JSON edits in the response, how do you apply the edits to get updated text? The simplest way is to use the `auto_apply` argument. By setting this to true, the returned response will have an extra field, `applied_text`, that contains the text with the edits applied. However, you can also easily apply edits programmatically. ## Programmatically Applying Edits[​](#programmatically-applying-edits "Direct link to Programmatically Applying Edits") Recall the Edit data structure: ``` { "id": <str, UUID>, // Opaque edit id, used to give feedback "sentence": <str>, // Unedited sentence "sentence_start": <int>, // Offset of sentence from start of text "start": <int>, // Offset of edit start relative to sentence "end": <int>, // Offset of edit end relative to sentence "replacement": <str>, // Suggested replacement "error_type": <str>, // Error type, see "Error Categories" "general_error_type": <str>, // See "Error Categories" } ``` When programmatically applying edits, go in *reverse* start offset (`sentence_start + start`) order so changes don't affect the offsets of the remaining edits. For example, consider the sentence: `Lets go to the housee.` where Sapling returns the following list of edits: ``` [ { 'sentence_start': 0, 'start': 0, 'end': 4, 'replacement': "Let's", ... }, { 'sentence_start': 0, 'start': 15, 'end': 21, 'replacement': 'house', ... } ] ``` The simplest way to apply the edits to your text is in reverse order: 1. Replace characters `15-21` with `house`. 2. Replace characters `0-4` with `Let's`. If the characters for `Lets` are replaced before `housee`, the offsets for other edits would need to be updated. ## Sample Code[​](#sample-code "Direct link to Sample Code") We provide sample code below for applying edits. A few things to keep in mind: * The `edits` array is ordered by starting position, though we include logic below to ensure this is the case. * For some languages where assignment is by reference, you will want to create a copy of the original string before modifying it. ``` text = str(text) edits = sorted(edits, key=lambda e: (e['sentence_start'] + e['start']), reverse=True) for edit in edits: start = edit['sentence_start'] + edit['start'] end = edit['sentence_start'] + edit['end'] if start > len(text) or end > len(text): print(f'Edit start:{start}/end:{end} outside of bounds of text:{text}') continue text = text[: start] + edit['replacement'] + text[end:] return text ``` ## Processing Files[​](#processing-files "Direct link to Processing Files") The Sapling API can be used to process files as well. Sapling's Edit API currently has a 100,000 character limit, so larger documents will need to be chunked. Sapling provides a pre-processing API endpoint that helps with chunking. This endpoint breaks long documents into pieces, prioritising splitting on things like page and paragraph breaks in order to preserve overall text context. ``` from sapling import SaplingClient file_name = '<FILE_TO_PROCESS>' api_key = '<api-key>' text = '' with open(file_name) as f: text = f.read().strip() client = SaplingClient(api_key=api_key) chunks = client.chunk_text(text, max_length=20000) for chunk in chunks: edits = client.edits(chunk, session_id=file_name) ``` More information about the Chunking/Preprocessing endpoint can be found [here](https://sapling.ai/docs/api/preprocess-overview.md). ## More Details[​](#more-details "Direct link to More Details") More detail on the API request options and response structure can be found [here](https://sapling.ai/docs/api/edits-overview.md). Documentation on `SaplingClient` is available on [Read the Docs](https://sapling.readthedocs.io/en/latest/api.html). --- # Status and Uptime You can check on the Sapling API status by checking our [status page](https://sapling.statuspage.io/). For a region-by-region breakdown of latency, see [here](https://sapling.ai/status?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs) after signing into Sapling. For customers, you can find Sapling's service level agreement (SLA) [here](https://sapling.ai/docs/api/sla.md). If you encounter any issues or downtime, please [contact us](https://sapling.ai/contact?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs). --- # Usage and Pricing Before you subscribe, your API key has a free trial quota of 50,000 characters/day, with 250,000 characters/month. [Subscribe](https://sapling.ai/api-checkout) for production access and [usage-based pricing](https://sapling.ai/docs/api/pricing); subscribed keys default to 10 million characters/day and 25 million characters/month (see [Rate Limits](https://sapling.ai/docs/rate_limits.md)). Higher usage volumes are discounted. API subscriptions started on or after August 12, 2026 require a one-time $5 minimum payment at signup; see [Billing](https://sapling.ai/docs/api/pricing#billing) for details. After creating your API key, you can view your usage at [https://sapling.ai/api\_settings](https://sapling.ai/api_settings?utm_source=sapling.ai\&utm_medium=documentation\&utm_campaign=apidocs) > Usage. It may take up to 1 hour for the usage to update. ---