# MetaPhrase complete documentation Canonical origin: https://metaphrase.msalman.com. Generated from the same request/response contracts as OpenAPI. Keys belong only in backend env METAPHRASE_API_KEY; never paste a secret key into an AI chat. # MetaPhrase developer API Bring six writing tools to your app with one consistent API and documentation your AI assistant can read. **Origin:** `https://metaphrase.msalman.com` **REST base:** `https://metaphrase.msalman.com/api/v1` **Authentication:** `Authorization: Bearer `, stored as `METAPHRASE_API_KEY` on your backend. Start with [the two-minute quickstart](https://metaphrase.msalman.com/docs/quickstart.md), create a key at [the dashboard](https://metaphrase.msalman.com/dashboard), or use the Scalar reference and **Try it** playground on this page. Grammar, citations and extractive key sentences work without a writing-provider account; generative tools depend on the operator's configured provider. ## Six endpoints - [Paraphrase](https://metaphrase.msalman.com/docs/paraphrase.md): seven modes, vocabulary strength and contextual alternatives. - [Grammar](https://metaphrase.msalman.com/docs/grammar.md): corrected text, categorized matches and UTF-16 offsets; no LLM. - [Detect](https://metaphrase.msalman.com/docs/detect.md): uncalibrated overall/sentence estimates with a mandatory disclaimer. - [Translate](https://metaphrase.msalman.com/docs/translate.md): 42 languages, source detection and RTL direction. - [Summarize](https://metaphrase.msalman.com/docs/summarize.md): paragraphs, bullets or exact key sentences. - [Cite](https://metaphrase.msalman.com/docs/cite.md): URL/DOI/ISBN/manual metadata, five styles, BibTeX and RIS; no LLM. ## For AI coding assistants Give your assistant one link: [the entire documentation](https://metaphrase.msalman.com/llms-full.txt). It includes every endpoint, parameter, example, error and integration guide. The short [llms.txt index](https://metaphrase.msalman.com/llms.txt) points to every Markdown page. Never paste your secret key into a chat; tell the assistant that the key is in `METAPHRASE_API_KEY` on your backend. ## Machine-readable files and downloads - [OpenAPI 3.1 JSON](https://metaphrase.msalman.com/openapi.json) - [OpenAPI 3.1 YAML](https://metaphrase.msalman.com/openapi.yaml) - [Postman collection](https://metaphrase.msalman.com/downloads/metaphrase.postman_collection.json) - [Insomnia export](https://metaphrase.msalman.com/downloads/metaphrase.insomnia.json) - [JavaScript/TypeScript SDK](https://metaphrase.msalman.com/downloads/metaphrase-sdk-1.0.0.tgz) - [Python SDK wheel](https://metaphrase.msalman.com/downloads/metaphrase_sdk-1.0.0-py3-none-any.whl) Every docs page has a Markdown version by adding `.md`, for example `/docs/paraphrase.md`. The docs home is `/docs.md` or `/docs/index.md`. Copy-page and AI-context actions use that same generated Markdown. Examples are illustrative; model wording and retrieved metadata can vary. --- # Quickstart Make your first successful API call in under two minutes. 1. Open [the developer dashboard](https://metaphrase.msalman.com/dashboard), create an account, and sign in. 2. Select **Create API key**, name it, and copy the full key. It is shown once. 3. Set `METAPHRASE_API_KEY` in your backend environment. Keep it out of frontend code, mobile apps, public repositories and AI chat prompts. ```bash export METAPHRASE_API_KEY='paste_your_new_secret_key_here' curl --fail-with-body --silent --show-error \ 'https://metaphrase.msalman.com/api/v1/grammar' \ -H "Authorization: Bearer $METAPHRASE_API_KEY" \ -H 'Content-Type: application/json' \ --data '{"text":"The best ideas deserves to be shared.","language":"en-US"}' ``` The response contains `result.text` equal to `The best ideas deserve to be shared.`, a `matches` array, `request_id`, and `usage.total_tokens: 0`. Grammar runs on MetaPhrase's own LanguageTool server and requires no writing-provider credential from you. An identical request may be served from cache. The origin is `https://metaphrase.msalman.com`. The REST base is `https://metaphrase.msalman.com/api/v1`. Endpoint paths are `/paraphrase`, `/grammar`, `/detect`, `/translate`, `/summarize`, and `/cite`. SDK `base_url` accepts the **origin**, without `/api/v1`. All six endpoints accept POST JSON with snake_case fields and the same Bearer key. Omit optional fields instead of sending null. Unknown fields are rejected. The current maximum input length is 20000 Unicode code points. Use [the endpoint references](https://metaphrase.msalman.com/docs/paraphrase.md), [OpenAPI JSON](https://metaphrase.msalman.com/openapi.json), or [the entire documentation](https://metaphrase.msalman.com/llms-full.txt). For a first paraphrase, replace `/grammar` with `/paraphrase` and use `{"text":"Good writing helps people understand an idea.","mode":"standard","synonym_strength":50}`. If it returns `provider_not_configured` (503), the operator must add a writing-provider key; changing your integration will not fix that deployment setting. Grammar, citation and `summarize` with `format: "key_sentences"` work independently. For a website, use the [complete HTML and backend proxy recipe](https://metaphrase.msalman.com/docs/recipe-html.md). Never call the secret-key API directly from frontend code. For a drop-in tool use [the public widget](https://metaphrase.msalman.com/docs/widget.md). --- # Authentication and API key safety Authenticate every REST call with a secret Bearer key stored on your backend. ```http Authorization: Bearer mp_live_<43 URL-safe characters> Content-Type: application/json ``` Create keys at [the dashboard](https://metaphrase.msalman.com/dashboard). A full key appears once; the database stores its HMAC hash and a visible prefix. Lost keys cannot be recovered. Create a replacement, update your backend environment, test it, then revoke the old key. Revocation blocks new calls immediately; an already accepted request may finish. Key names and usage remain visible after revocation. Use environment variable `METAPHRASE_API_KEY`. Never use `NEXT_PUBLIC_METAPHRASE_API_KEY`, `VITE_*`, a frontend `.env`, mobile build constants, public JavaScript, browser storage, a repository, or an AI chat prompt for a secret key. AI integration prompts should mention the variable name, never its value. The Scalar playground sends a key you explicitly enter to this same API origin and does not persist it as saved authentication. CORS is enabled on the REST API for compatibility with backend tooling and development. It is not a permission to expose a secret key in a browser. A browser sends text to **your backend proxy**; that backend reads the secret environment variable and calls MetaPhrase. ```text Browser/mobile UI → your authenticated or rate-limited backend → MetaPhrase REST API ``` [The HTML recipe](https://metaphrase.msalman.com/docs/recipe-html.md) contains a complete Node server and page. [Next.js](https://metaphrase.msalman.com/docs/recipe-nextjs.md), [WordPress](https://metaphrase.msalman.com/docs/recipe-wordpress.md), [Laravel](https://metaphrase.msalman.com/docs/recipe-laravel.md), and [Python backends](https://metaphrase.msalman.com/docs/recipe-python.md) have dedicated examples. An API key belongs only in those server files. Dashboard accounts use a 12-character minimum password, salted scrypt hashes and opaque HttpOnly, Secure, SameSite=Lax session cookies. Dashboard mutations require the same origin. API calls use Bearer keys, not dashboard cookies. Account creation is limited by IP; API keys inherit their account's plan. Free defaults: five active keys, 15 calls per key per 60 seconds, 500 accepted calls per key per UTC month. Pro placeholders: 20 keys, 120/60 seconds, 25,000/month. Plans can be changed by the operator. No payments are collected. Limits are shared across all tools using a key. Requests and outputs are not stored in the PostgreSQL usage log; it records metadata such as tokens, tool, key ID, status and estimated provider cost. Identical results are cached in private Redis for up to 24 hours. Submitted text is sent to the configured provider for generative tools; grammar, citations and extractive summaries do not use an LLM. Keep your own application logs from recording secrets or private writing. --- # paraphrase Rewrite text in one of seven modes while preserving its meaning. ## Endpoint and method `POST https://metaphrase.msalman.com/api/v1/paraphrase` ## Authentication Send `Authorization: Bearer ` and `Content-Type: application/json`. Create a key at [the dashboard](https://metaphrase.msalman.com/dashboard). Store it in backend environment variable `METAPHRASE_API_KEY`; never expose it in a browser, mobile binary, public repository, or widget. See [authentication and a working backend proxy](https://metaphrase.msalman.com/docs/authentication.md). ## Request parameters JSON uses snake_case. Unknown fields are rejected. Omit optional values instead of sending null. Maximum text length: **20,000 Unicode code points**. | Name | Type | Required | Default | Allowed values / range | Max length / items | | --- | --- | --- | --- | --- | --- | | `text` | string | Yes | — | — | 20000 | | `mode` | string | No | "standard" | standard, fluent, formal, simple, creative, shorten, expand | — | | `synonym_strength` | integer | No | 50 | 0 to 100 | — | | `action` | string | No | "rewrite" | rewrite, alternatives | — | | `target_word` | string | If action=alternatives | — | — | 80 | | `stream` | boolean | No | false | — | — | - **`text`**: Plain text. Limit counts Unicode code points. - **`mode`**: Rewrite style. All seven modes preserve source language, names, facts and meaning. - **`synonym_strength`**: Vocabulary change strength, from 0 (minimal) to 100 (strong). - **`action`**: rewrite returns rewritten text; alternatives returns context-appropriate alternatives for target_word. - **`target_word`**: Required for action=alternatives. Must appear in text; returns up to eight alternatives, at most 80 characters each. - **`stream`**: Use true for SSE on paraphrase/rewrite, translate or summarize. Other operations require false; alternatives always returns JSON. All modes preserve the source language. `synonym_strength` controls vocabulary changes. `changes` contains output segments with `changed=true` for newly written words; concatenating segments reconstructs `result.text`. For contextual word alternatives, send `action=alternatives` and `target_word` that appears in `text`; it always returns JSON, even if `stream=true`. ## Full example request ```json { "text": "Good writing helps people understand an idea.", "mode": "standard", "synonym_strength": 50, "action": "rewrite", "stream": false } ``` Word alternatives request (same endpoint): ```json { "text": "Good writing helps people understand an idea.", "action": "alternatives", "target_word": "Good", "mode": "standard", "synonym_strength": 50, "stream": false } ``` ## Full example response HTTP 200, `application/json`. This is an illustrative response; writing, source metadata, token counts and cost can vary. `request_id` is a server-generated UUID. Cache hits use `usage.cached=true` and report zero new tokens/cost. `usage_estimated=true` means the provider did not supply exact token counts. ```json { "request_id": "019b8df0-1234-4000-8000-000000000001", "tool": "paraphrase", "result": { "text": "Clear writing makes ideas easier to understand.", "mode": "standard", "synonym_strength": 50, "changes": [ { "text": "Clear writing makes ideas easier to understand.", "changed": true } ] }, "usage": { "input_tokens": 60, "output_tokens": 20, "total_tokens": 80, "estimated_cost_usd": 0.000016, "usage_estimated": false, "cached": false, "provider": "primary", "model": "gpt-6-luna" }, "input_words": 7, "output_words": 7 } ``` For alternatives, the same envelope contains `result.text` equal to the input, `changes: []`, `target_word`, and up to eight `alternatives`, for example `["Clear", "Effective", "Thoughtful", "Strong", "Skilled"]`. Usage is metered in the usual envelope. ## Errors: cause and fix Every error uses this JSON shape and a link to its exact fix: ```json { "error": { "code": "unauthorized", "message": "A valid API key is required.", "docs_url": "https://metaphrase.msalman.com/docs/errors#unauthorized" } } ``` | Code | HTTP | Cause | Fix | | --- | --- | --- | --- | | [`invalid_request`](https://metaphrase.msalman.com/docs/errors#invalid_request) | 400 | Check the request fields and their allowed values. | Send the documented JSON fields with the correct types. Unknown fields are rejected. | | [`invalid_json`](https://metaphrase.msalman.com/docs/errors#invalid_json) | 400 | The request body is not valid JSON. | Serialize the request with JSON.stringify or your language's JSON encoder. | | [`unsupported_media_type`](https://metaphrase.msalman.com/docs/errors#unsupported_media_type) | 415 | Send application/json content. | Set Content-Type: application/json. File uploads use their separate documented route. | | [`body_too_large`](https://metaphrase.msalman.com/docs/errors#body_too_large) | 413 | The request body exceeds the allowed size. | Reduce the payload and split long documents at sentence boundaries. | | [`text_too_long`](https://metaphrase.msalman.com/docs/errors#text_too_long) | 413 | The text exceeds the configured character limit. | Use the max_input_chars value in the API specification and split longer documents. | | [`unauthorized`](https://metaphrase.msalman.com/docs/errors#unauthorized) | 401 | A valid API key is required. | Send Authorization: Bearer YOUR_KEY. Create a new key if the old one was revoked. | | [`forbidden`](https://metaphrase.msalman.com/docs/errors#forbidden) | 403 | This action is not available to this account. | Check account status and the key's access permissions. | | [`rate_limited`](https://metaphrase.msalman.com/docs/errors#rate_limited) | 429 | Too many requests. Please wait and try again. | Wait for Retry-After seconds and use exponential backoff with jitter. | | [`quota_exceeded`](https://metaphrase.msalman.com/docs/errors#quota_exceeded) | 429 | The monthly usage quota has been reached. | Wait for the next UTC calendar month or ask an administrator to change the plan. | | [`service_unavailable`](https://metaphrase.msalman.com/docs/errors#service_unavailable) | 503 | A required service is temporarily unavailable. | Wait and retry. The operator should check Redis, PostgreSQL, or the tool's health. | | [`not_found`](https://metaphrase.msalman.com/docs/errors#not_found) | 404 | This resource does not exist. | Check the API version, endpoint name, and resource identifier. | | [`method_not_allowed`](https://metaphrase.msalman.com/docs/errors#method_not_allowed) | 405 | This HTTP method is not supported. | Use POST for processing endpoints and the method documented for dashboard routes. | | [`internal_error`](https://metaphrase.msalman.com/docs/errors#internal_error) | 500 | The request could not be completed. | Retry once. If it persists, provide the X-Request-Id header to the operator. | | [`budget_exceeded`](https://metaphrase.msalman.com/docs/errors#budget_exceeded) | 429 | The writing service has reached its configured spending limit. | The operator must review usage and adjust the configured budget before more paid requests can run. | | [`provider_not_configured`](https://metaphrase.msalman.com/docs/errors#provider_not_configured) | 503 | The writing provider is not configured yet. | The operator must configure LLM_PRIMARY_API_KEY or a fallback provider in .env and recreate the web container. | | [`upstream_unavailable`](https://metaphrase.msalman.com/docs/errors#upstream_unavailable) | 503 | The writing provider is temporarily unavailable. | Retry with backoff. The operator can inspect provider availability without logging submitted text. | | [`upstream_timeout`](https://metaphrase.msalman.com/docs/errors#upstream_timeout) | 504 | The writing provider took too long to respond. | Retry a shorter input or wait before retrying. Do not append a retry to partial streamed output. | | [`upstream_error`](https://metaphrase.msalman.com/docs/errors#upstream_error) | 502 | The writing provider returned an invalid response. | Retry once with a shorter input. If it continues, the operator should check the configured model's compatibility. | | [`output_truncated`](https://metaphrase.msalman.com/docs/errors#output_truncated) | 502 | The response reached its output limit before finishing. | Use a shorter input or ask the operator to adjust the tool's output cap and chunk size. | For streaming requests, failures after HTTP headers are sent arrive as an SSE `error` event with this same error object. Never treat partial text as a completed result or append output from a retry. ## Rate limits and quotas Defaults: Free **15 requests per 60 seconds** and **500 accepted calls per key per UTC calendar month**; Pro placeholders **120/60 seconds** and **25,000/month**. The operator can change plans; response headers are authoritative. Calls to all tools share a key's limits. Accepted cache hits and upstream failures consume quota. Authentication/schema failures and provider-not-configured preflight failures do not. `X-RateLimit-Remaining`, `X-RateLimit-Reset`, `X-Quota-Remaining` and `X-Quota-Reset` show remaining capacity and Unix-second reset times. For HTTP 429 wait `Retry-After` seconds; quota/budget limits need a reset or operator action. Requests may also receive 429 when the shared processing slots are busy. See [retries and long texts](https://metaphrase.msalman.com/docs/reliability.md). ## Code samples These are complete backend programs, not frontend key examples. JavaScript fetch and Node.js are separate options; the latter uses the official SDK. Flutter UI must call your own backend; the Dart sample runs on a backend/CLI. See [mobile integration](https://metaphrase.msalman.com/docs/recipe-mobile.md). ### cURL Save as `request.sh`. Run: `bash request.sh`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```bash #!/usr/bin/env bash set -euo pipefail : "${METAPHRASE_API_KEY:?Set METAPHRASE_API_KEY in your backend environment}" BASE_URL="${METAPHRASE_BASE_URL:-https://metaphrase.msalman.com}" curl --fail-with-body --silent --show-error \ --request POST "${BASE_URL%/}/api/v1/paraphrase" \ --header "Authorization: Bearer $METAPHRASE_API_KEY" \ --header 'Content-Type: application/json' \ --data-binary @- <<'JSON' { "text": "Good writing helps people understand an idea.", "mode": "standard", "synonym_strength": 50, "action": "rewrite", "stream": false } JSON ``` ### JavaScript (fetch) Save as `request.mjs`. Run: `node request.mjs (Node.js 22+; backend only)`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```javascript const key = process.env.METAPHRASE_API_KEY; if (!key) throw new Error("Set METAPHRASE_API_KEY on your backend"); const base = (process.env.METAPHRASE_BASE_URL ?? "https://metaphrase.msalman.com").replace(/\/$/, ""); const payload = { "text": "Good writing helps people understand an idea.", "mode": "standard", "synonym_strength": 50, "action": "rewrite", "stream": false }; const response = await fetch(base + "/api/v1/paraphrase", { method: "POST", headers: { Authorization: "Bearer " + key, "Content-Type": "application/json" }, body: JSON.stringify(payload), signal: AbortSignal.timeout(250000), }); const result = await response.json(); if (!response.ok) { console.error(response.status, result.error.code, result.error.message, result.error.docs_url); process.exitCode = 1; } else { console.log(JSON.stringify(result, null, 2)); } ``` ### Node.js Save as `request-sdk.mjs`. Run: `npm install https://metaphrase.msalman.com/downloads/metaphrase-sdk-1.0.0.tgz && node request-sdk.mjs`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```javascript import { MetaPhrase, MetaPhraseError } from "@metaphrase/sdk"; const client = new MetaPhrase({ api_key: process.env.METAPHRASE_API_KEY, base_url: process.env.METAPHRASE_BASE_URL ?? "https://metaphrase.msalman.com", }); try { const result = await client.paraphrase({ "text": "Good writing helps people understand an idea.", "mode": "standard", "synonym_strength": 50, "action": "rewrite", "stream": false }); console.log(JSON.stringify(result, null, 2)); } catch (error) { if (error instanceof MetaPhraseError) console.error(error.status, error.code, error.message, error.docs_url); else console.error(error); process.exitCode = 1; } ``` ### Python Save as `request.py`. Run: `python3 request.py (Python 3.11+; standard library only)`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```python import json import os import sys from urllib.request import Request, urlopen from urllib.error import HTTPError key = os.environ["METAPHRASE_API_KEY"] base = os.environ.get("METAPHRASE_BASE_URL", "https://metaphrase.msalman.com").rstrip("/") payload = json.loads("{\"text\":\"Good writing helps people understand an idea.\",\"mode\":\"standard\",\"synonym_strength\":50,\"action\":\"rewrite\",\"stream\":false}") request = Request( base + "/api/v1/paraphrase", data=json.dumps(payload, ensure_ascii=False).encode("utf-8"), headers={"Authorization": "Bearer " + key, "Content-Type": "application/json"}, method="POST", ) try: with urlopen(request, timeout=250) as response: print(json.dumps(json.load(response), ensure_ascii=False, indent=2)) except HTTPError as error: print(error.code, error.read().decode("utf-8"), file=sys.stderr) sys.exit(1) ``` ### PHP Save as `request.php`. Run: `php request.php (PHP 8.2+ with the cURL extension)`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 250, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key, 'Content-Type: application/json'], CURLOPT_POSTFIELDS => json_encode($payload, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE), ]); $body = curl_exec($curl); if ($body === false) { throw new RuntimeException(curl_error($curl)); } $status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE); curl_close($curl); $result = json_decode($body, true, 512, JSON_THROW_ON_ERROR); echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . PHP_EOL; if ($status >= 400) { exit(1); } ``` ### Java Save as `MetaPhraseExample.java`. Run: `javac -encoding UTF-8 MetaPhraseExample.java && java MetaPhraseExample (JDK 17+)`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Duration; public class MetaPhraseExample { public static void main(String[] args) throws Exception { String key = System.getenv("METAPHRASE_API_KEY"); if (key == null || key.isBlank()) throw new IllegalStateException("Set METAPHRASE_API_KEY"); String base = System.getenv().getOrDefault("METAPHRASE_BASE_URL", "https://metaphrase.msalman.com").replaceAll("/+$", ""); String payload = """ { "text": "Good writing helps people understand an idea.", "mode": "standard", "synonym_strength": 50, "action": "rewrite", "stream": false } """; HttpRequest request = HttpRequest.newBuilder(URI.create(base + "/api/v1/paraphrase")) .timeout(Duration.ofSeconds(250)) .header("Authorization", "Bearer " + key) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(payload)) .build(); HttpResponse response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); if (response.statusCode() >= 400) throw new java.io.IOException("HTTP " + response.statusCode() + ": " + response.body()); } } ``` ### C# Save as `Program.cs`. Run: `dotnet new console -n MetaPhraseExample; replace its Program.cs, then dotnet run --project MetaPhraseExample (.NET 8+)`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```csharp using System.Net.Http.Headers; using System.Text; string key = Environment.GetEnvironmentVariable("METAPHRASE_API_KEY") ?? throw new InvalidOperationException("Set METAPHRASE_API_KEY"); string baseUrl = (Environment.GetEnvironmentVariable("METAPHRASE_BASE_URL") ?? "https://metaphrase.msalman.com").TrimEnd('/'); string payload = """ { "text": "Good writing helps people understand an idea.", "mode": "standard", "synonym_strength": 50, "action": "rewrite", "stream": false } """; using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(250) }; client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", key); using var content = new StringContent(payload, Encoding.UTF8, "application/json"); using var response = await client.PostAsync(baseUrl + "/api/v1/paraphrase", content); string result = await response.Content.ReadAsStringAsync(); Console.WriteLine(result); if (!response.IsSuccessStatusCode) throw new HttpRequestException($"HTTP {(int)response.StatusCode}: {result}"); ``` ### Go Save as `main.go`. Run: `go run main.go (Go 1.22+; standard library only)`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```go package main import ( "bytes" "fmt" "io" "net/http" "os" "strings" "time" ) func main() { key := os.Getenv("METAPHRASE_API_KEY") if key == "" { panic("Set METAPHRASE_API_KEY") } base := os.Getenv("METAPHRASE_BASE_URL") if base == "" { base = "https://metaphrase.msalman.com" } payload := []byte(`{ "text": "Good writing helps people understand an idea.", "mode": "standard", "synonym_strength": 50, "action": "rewrite", "stream": false }`) request, err := http.NewRequest(http.MethodPost, strings.TrimRight(base, "/") + "/api/v1/paraphrase", bytes.NewReader(payload)) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + key) request.Header.Set("Content-Type", "application/json") client := &http.Client{Timeout: 250 * time.Second} response, err := client.Do(request) if err != nil { panic(err) } defer response.Body.Close() body, err := io.ReadAll(response.Body) if err != nil { panic(err) } fmt.Println(string(body)) if response.StatusCode >= 400 { os.Exit(1) } } ``` ### Flutter / Dart Save as `request.dart`. Run: `dart run request.dart (Dart 3+ backend/CLI; Flutter UI calls your backend proxy)`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```dart import 'dart:convert'; import 'dart:io'; Future main() async { final key = Platform.environment['METAPHRASE_API_KEY']; if (key == null || key.isEmpty) throw StateError('Set METAPHRASE_API_KEY on your backend'); final base = (Platform.environment['METAPHRASE_BASE_URL'] ?? 'https://metaphrase.msalman.com').replaceFirst(RegExp(r'/+$'), ''); final payload = jsonDecode(r'''{"text":"Good writing helps people understand an idea.","mode":"standard","synonym_strength":50,"action":"rewrite","stream":false}'''); final client = HttpClient()..connectionTimeout = const Duration(seconds: 30); try { final request = await client.postUrl(Uri.parse('$base/api/v1/paraphrase')); request.headers.set(HttpHeaders.authorizationHeader, 'Bearer $key'); request.headers.contentType = ContentType.json; request.write(jsonEncode(payload)); final response = await request.close().timeout(const Duration(seconds: 250)); final body = await utf8.decoder.bind(response).join(); stdout.writeln(body); if (response.statusCode >= 400) exitCode = 1; } finally { client.close(force: true); } } ``` --- # grammar Find language issues and return suggestions with corrected text, without an LLM. ## Endpoint and method `POST https://metaphrase.msalman.com/api/v1/grammar` ## Authentication Send `Authorization: Bearer ` and `Content-Type: application/json`. Create a key at [the dashboard](https://metaphrase.msalman.com/dashboard). Store it in backend environment variable `METAPHRASE_API_KEY`; never expose it in a browser, mobile binary, public repository, or widget. See [authentication and a working backend proxy](https://metaphrase.msalman.com/docs/authentication.md). ## Request parameters JSON uses snake_case. Unknown fields are rejected. Omit optional values instead of sending null. Maximum text length: **20,000 Unicode code points**. | Name | Type | Required | Default | Allowed values / range | Max length / items | | --- | --- | --- | --- | --- | --- | | `text` | string | Yes | — | — | 20000 | | `language` | string | No | "en-US" | auto, ar, ast-ES, be-BY, br-FR, ca-ES, ca-ES-valencia, ca-ES-balear, da-DK, de, de-DE, de-AT, de-CH, de-DE-x-simple-language, el-GR, en, en-US, en-GB, en-AU, en-CA, en-NZ, en-ZA, eo, es, es-AR, fa, fr, fr-CA, fr-CH, fr-BE, ga-IE, gl-ES, it, ja-JP, km-KH, nl, nl-BE, pl-PL, pt, pt-PT, pt-BR, pt-AO, pt-MZ, ro-RO, ru-RU, sk-SK, sl-SI, sv, ta-IN, tl-PH, uk-UA, zh-CN, crh-UA, nl-NL, de-DE-x-simple-language-DE, es-ES, it-IT, fa-IR, sv-SE, de-LU, fr-FR | — | | `stream` | boolean | No | false | false | — | - **`text`**: Plain text. Limit counts Unicode code points. - **`language`**: A LanguageTool language code, or auto. LanguageTool supports only the listed codes; Urdu/Hindi are translation targets, not grammar languages. - **`stream`**: Use true for SSE on paraphrase/rewrite, translate or summarize. Other operations require false; alternatives always returns JSON. Offsets and lengths use **UTF-16 code units**, matching JavaScript. Python strings use Unicode code points: use `text.encode('utf-16-le')[offset * 2:(offset + length) * 2].decode('utf-16-le')` to extract a match. `original_text` is unchanged. `text` applies the first replacement for non-overlapping matches. `corrections_count` counts applicable fixes, while `matches` may also contain overlaps or issues with no replacement. ## Full example request ```json { "text": "The best ideas deserves to be shared.", "language": "en-US", "stream": false } ``` ## Full example response HTTP 200, `application/json`. This is an illustrative response; writing, source metadata, token counts and cost can vary. `request_id` is a server-generated UUID. Cache hits use `usage.cached=true` and report zero new tokens/cost. `usage_estimated=true` means the provider did not supply exact token counts. ```json { "request_id": "019b8df0-1234-4000-8000-000000000002", "tool": "grammar", "result": { "text": "The best ideas deserve to be shared.", "original_text": "The best ideas deserves to be shared.", "language": "en-US", "matches": [ { "id": "0-15", "offset": 15, "length": 8, "message": "The verb form ‘deserves’ does not seem to match the subject ‘ideas’.", "short_message": "", "category": "grammar", "rule_id": "SUBJECT_VERB_AGREEMENT_PLURAL", "replacements": [ "deserve", "deserved", "will deserve" ], "original": "deserves" } ], "corrections_count": 1 }, "usage": { "input_tokens": 0, "output_tokens": 0, "total_tokens": 0, "estimated_cost_usd": 0, "usage_estimated": false, "cached": false, "provider": "self_hosted", "model": "LanguageTool 6.8" }, "input_words": 7, "output_words": 7 } ``` ## Errors: cause and fix Every error uses this JSON shape and a link to its exact fix: ```json { "error": { "code": "unauthorized", "message": "A valid API key is required.", "docs_url": "https://metaphrase.msalman.com/docs/errors#unauthorized" } } ``` | Code | HTTP | Cause | Fix | | --- | --- | --- | --- | | [`invalid_request`](https://metaphrase.msalman.com/docs/errors#invalid_request) | 400 | Check the request fields and their allowed values. | Send the documented JSON fields with the correct types. Unknown fields are rejected. | | [`invalid_json`](https://metaphrase.msalman.com/docs/errors#invalid_json) | 400 | The request body is not valid JSON. | Serialize the request with JSON.stringify or your language's JSON encoder. | | [`unsupported_media_type`](https://metaphrase.msalman.com/docs/errors#unsupported_media_type) | 415 | Send application/json content. | Set Content-Type: application/json. File uploads use their separate documented route. | | [`body_too_large`](https://metaphrase.msalman.com/docs/errors#body_too_large) | 413 | The request body exceeds the allowed size. | Reduce the payload and split long documents at sentence boundaries. | | [`text_too_long`](https://metaphrase.msalman.com/docs/errors#text_too_long) | 413 | The text exceeds the configured character limit. | Use the max_input_chars value in the API specification and split longer documents. | | [`unauthorized`](https://metaphrase.msalman.com/docs/errors#unauthorized) | 401 | A valid API key is required. | Send Authorization: Bearer YOUR_KEY. Create a new key if the old one was revoked. | | [`forbidden`](https://metaphrase.msalman.com/docs/errors#forbidden) | 403 | This action is not available to this account. | Check account status and the key's access permissions. | | [`rate_limited`](https://metaphrase.msalman.com/docs/errors#rate_limited) | 429 | Too many requests. Please wait and try again. | Wait for Retry-After seconds and use exponential backoff with jitter. | | [`quota_exceeded`](https://metaphrase.msalman.com/docs/errors#quota_exceeded) | 429 | The monthly usage quota has been reached. | Wait for the next UTC calendar month or ask an administrator to change the plan. | | [`service_unavailable`](https://metaphrase.msalman.com/docs/errors#service_unavailable) | 503 | A required service is temporarily unavailable. | Wait and retry. The operator should check Redis, PostgreSQL, or the tool's health. | | [`not_found`](https://metaphrase.msalman.com/docs/errors#not_found) | 404 | This resource does not exist. | Check the API version, endpoint name, and resource identifier. | | [`method_not_allowed`](https://metaphrase.msalman.com/docs/errors#method_not_allowed) | 405 | This HTTP method is not supported. | Use POST for processing endpoints and the method documented for dashboard routes. | | [`internal_error`](https://metaphrase.msalman.com/docs/errors#internal_error) | 500 | The request could not be completed. | Retry once. If it persists, provide the X-Request-Id header to the operator. | | [`upstream_timeout`](https://metaphrase.msalman.com/docs/errors#upstream_timeout) | 504 | The writing provider took too long to respond. | Retry a shorter input or wait before retrying. Do not append a retry to partial streamed output. | | [`upstream_error`](https://metaphrase.msalman.com/docs/errors#upstream_error) | 502 | The writing provider returned an invalid response. | Retry once with a shorter input. If it continues, the operator should check the configured model's compatibility. | For streaming requests, failures after HTTP headers are sent arrive as an SSE `error` event with this same error object. Never treat partial text as a completed result or append output from a retry. ## Rate limits and quotas Defaults: Free **15 requests per 60 seconds** and **500 accepted calls per key per UTC calendar month**; Pro placeholders **120/60 seconds** and **25,000/month**. The operator can change plans; response headers are authoritative. Calls to all tools share a key's limits. Accepted cache hits and upstream failures consume quota. Authentication/schema failures and provider-not-configured preflight failures do not. `X-RateLimit-Remaining`, `X-RateLimit-Reset`, `X-Quota-Remaining` and `X-Quota-Reset` show remaining capacity and Unix-second reset times. For HTTP 429 wait `Retry-After` seconds; quota/budget limits need a reset or operator action. Requests may also receive 429 when the shared processing slots are busy. See [retries and long texts](https://metaphrase.msalman.com/docs/reliability.md). ## Code samples These are complete backend programs, not frontend key examples. JavaScript fetch and Node.js are separate options; the latter uses the official SDK. Flutter UI must call your own backend; the Dart sample runs on a backend/CLI. See [mobile integration](https://metaphrase.msalman.com/docs/recipe-mobile.md). ### cURL Save as `request.sh`. Run: `bash request.sh`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```bash #!/usr/bin/env bash set -euo pipefail : "${METAPHRASE_API_KEY:?Set METAPHRASE_API_KEY in your backend environment}" BASE_URL="${METAPHRASE_BASE_URL:-https://metaphrase.msalman.com}" curl --fail-with-body --silent --show-error \ --request POST "${BASE_URL%/}/api/v1/grammar" \ --header "Authorization: Bearer $METAPHRASE_API_KEY" \ --header 'Content-Type: application/json' \ --data-binary @- <<'JSON' { "text": "The best ideas deserves to be shared.", "language": "en-US", "stream": false } JSON ``` ### JavaScript (fetch) Save as `request.mjs`. Run: `node request.mjs (Node.js 22+; backend only)`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```javascript const key = process.env.METAPHRASE_API_KEY; if (!key) throw new Error("Set METAPHRASE_API_KEY on your backend"); const base = (process.env.METAPHRASE_BASE_URL ?? "https://metaphrase.msalman.com").replace(/\/$/, ""); const payload = { "text": "The best ideas deserves to be shared.", "language": "en-US", "stream": false }; const response = await fetch(base + "/api/v1/grammar", { method: "POST", headers: { Authorization: "Bearer " + key, "Content-Type": "application/json" }, body: JSON.stringify(payload), signal: AbortSignal.timeout(250000), }); const result = await response.json(); if (!response.ok) { console.error(response.status, result.error.code, result.error.message, result.error.docs_url); process.exitCode = 1; } else { console.log(JSON.stringify(result, null, 2)); } ``` ### Node.js Save as `request-sdk.mjs`. Run: `npm install https://metaphrase.msalman.com/downloads/metaphrase-sdk-1.0.0.tgz && node request-sdk.mjs`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```javascript import { MetaPhrase, MetaPhraseError } from "@metaphrase/sdk"; const client = new MetaPhrase({ api_key: process.env.METAPHRASE_API_KEY, base_url: process.env.METAPHRASE_BASE_URL ?? "https://metaphrase.msalman.com", }); try { const result = await client.grammar({ "text": "The best ideas deserves to be shared.", "language": "en-US", "stream": false }); console.log(JSON.stringify(result, null, 2)); } catch (error) { if (error instanceof MetaPhraseError) console.error(error.status, error.code, error.message, error.docs_url); else console.error(error); process.exitCode = 1; } ``` ### Python Save as `request.py`. Run: `python3 request.py (Python 3.11+; standard library only)`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```python import json import os import sys from urllib.request import Request, urlopen from urllib.error import HTTPError key = os.environ["METAPHRASE_API_KEY"] base = os.environ.get("METAPHRASE_BASE_URL", "https://metaphrase.msalman.com").rstrip("/") payload = json.loads("{\"text\":\"The best ideas deserves to be shared.\",\"language\":\"en-US\",\"stream\":false}") request = Request( base + "/api/v1/grammar", data=json.dumps(payload, ensure_ascii=False).encode("utf-8"), headers={"Authorization": "Bearer " + key, "Content-Type": "application/json"}, method="POST", ) try: with urlopen(request, timeout=250) as response: print(json.dumps(json.load(response), ensure_ascii=False, indent=2)) except HTTPError as error: print(error.code, error.read().decode("utf-8"), file=sys.stderr) sys.exit(1) ``` ### PHP Save as `request.php`. Run: `php request.php (PHP 8.2+ with the cURL extension)`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 250, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key, 'Content-Type: application/json'], CURLOPT_POSTFIELDS => json_encode($payload, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE), ]); $body = curl_exec($curl); if ($body === false) { throw new RuntimeException(curl_error($curl)); } $status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE); curl_close($curl); $result = json_decode($body, true, 512, JSON_THROW_ON_ERROR); echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . PHP_EOL; if ($status >= 400) { exit(1); } ``` ### Java Save as `MetaPhraseExample.java`. Run: `javac -encoding UTF-8 MetaPhraseExample.java && java MetaPhraseExample (JDK 17+)`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Duration; public class MetaPhraseExample { public static void main(String[] args) throws Exception { String key = System.getenv("METAPHRASE_API_KEY"); if (key == null || key.isBlank()) throw new IllegalStateException("Set METAPHRASE_API_KEY"); String base = System.getenv().getOrDefault("METAPHRASE_BASE_URL", "https://metaphrase.msalman.com").replaceAll("/+$", ""); String payload = """ { "text": "The best ideas deserves to be shared.", "language": "en-US", "stream": false } """; HttpRequest request = HttpRequest.newBuilder(URI.create(base + "/api/v1/grammar")) .timeout(Duration.ofSeconds(250)) .header("Authorization", "Bearer " + key) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(payload)) .build(); HttpResponse response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); if (response.statusCode() >= 400) throw new java.io.IOException("HTTP " + response.statusCode() + ": " + response.body()); } } ``` ### C# Save as `Program.cs`. Run: `dotnet new console -n MetaPhraseExample; replace its Program.cs, then dotnet run --project MetaPhraseExample (.NET 8+)`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```csharp using System.Net.Http.Headers; using System.Text; string key = Environment.GetEnvironmentVariable("METAPHRASE_API_KEY") ?? throw new InvalidOperationException("Set METAPHRASE_API_KEY"); string baseUrl = (Environment.GetEnvironmentVariable("METAPHRASE_BASE_URL") ?? "https://metaphrase.msalman.com").TrimEnd('/'); string payload = """ { "text": "The best ideas deserves to be shared.", "language": "en-US", "stream": false } """; using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(250) }; client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", key); using var content = new StringContent(payload, Encoding.UTF8, "application/json"); using var response = await client.PostAsync(baseUrl + "/api/v1/grammar", content); string result = await response.Content.ReadAsStringAsync(); Console.WriteLine(result); if (!response.IsSuccessStatusCode) throw new HttpRequestException($"HTTP {(int)response.StatusCode}: {result}"); ``` ### Go Save as `main.go`. Run: `go run main.go (Go 1.22+; standard library only)`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```go package main import ( "bytes" "fmt" "io" "net/http" "os" "strings" "time" ) func main() { key := os.Getenv("METAPHRASE_API_KEY") if key == "" { panic("Set METAPHRASE_API_KEY") } base := os.Getenv("METAPHRASE_BASE_URL") if base == "" { base = "https://metaphrase.msalman.com" } payload := []byte(`{ "text": "The best ideas deserves to be shared.", "language": "en-US", "stream": false }`) request, err := http.NewRequest(http.MethodPost, strings.TrimRight(base, "/") + "/api/v1/grammar", bytes.NewReader(payload)) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + key) request.Header.Set("Content-Type", "application/json") client := &http.Client{Timeout: 250 * time.Second} response, err := client.Do(request) if err != nil { panic(err) } defer response.Body.Close() body, err := io.ReadAll(response.Body) if err != nil { panic(err) } fmt.Println(string(body)) if response.StatusCode >= 400 { os.Exit(1) } } ``` ### Flutter / Dart Save as `request.dart`. Run: `dart run request.dart (Dart 3+ backend/CLI; Flutter UI calls your backend proxy)`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```dart import 'dart:convert'; import 'dart:io'; Future main() async { final key = Platform.environment['METAPHRASE_API_KEY']; if (key == null || key.isEmpty) throw StateError('Set METAPHRASE_API_KEY on your backend'); final base = (Platform.environment['METAPHRASE_BASE_URL'] ?? 'https://metaphrase.msalman.com').replaceFirst(RegExp(r'/+$'), ''); final payload = jsonDecode(r'''{"text":"The best ideas deserves to be shared.","language":"en-US","stream":false}'''); final client = HttpClient()..connectionTimeout = const Duration(seconds: 30); try { final request = await client.postUrl(Uri.parse('$base/api/v1/grammar')); request.headers.set(HttpHeaders.authorizationHeader, 'Bearer $key'); request.headers.contentType = ContentType.json; request.write(jsonEncode(payload)); final response = await request.close().timeout(const Duration(seconds: 250)); final body = await utf8.decoder.bind(response).join(); stdout.writeln(body); if (response.statusCode >= 400) exitCode = 1; } finally { client.close(force: true); } } ``` --- # detect Estimate AI-like writing cues overall and for each sentence; results may be wrong. ## Endpoint and method `POST https://metaphrase.msalman.com/api/v1/detect` ## Authentication Send `Authorization: Bearer ` and `Content-Type: application/json`. Create a key at [the dashboard](https://metaphrase.msalman.com/dashboard). Store it in backend environment variable `METAPHRASE_API_KEY`; never expose it in a browser, mobile binary, public repository, or widget. See [authentication and a working backend proxy](https://metaphrase.msalman.com/docs/authentication.md). ## Request parameters JSON uses snake_case. Unknown fields are rejected. Omit optional values instead of sending null. Maximum text length: **20,000 Unicode code points**. | Name | Type | Required | Default | Allowed values / range | Max length / items | | --- | --- | --- | --- | --- | --- | | `text` | string | Yes | — | — | 20000 | | `stream` | boolean | No | false | false | — | - **`text`**: Plain text. Limit counts Unicode code points. At least 50 words are required. Scores are uncalibrated estimates, not authorship evidence. - **`stream`**: Use true for SSE on paraphrase/rewrite, translate or summarize. Other operations require false; alternatives always returns JSON. This is an **uncalibrated LLM heuristic**, not a validated probability of authorship. At least 50 words are required. Always display `disclaimer` with every result. Do not make decisions about a person from this score alone. Sentence offsets and lengths use UTF-16 units; very long sentences may be divided into bounded spans. ## Full example request ```json { "text": "The research team spent several long months carefully studying how local people use public spaces in their neighborhood and listening to residents describe their daily routines. They found that shade, comfortable seating, and safe crossings mattered more to participants than expensive decorative features, so the final proposal focused on those practical needs.", "stream": false } ``` ## Full example response HTTP 200, `application/json`. This is an illustrative response; writing, source metadata, token counts and cost can vary. `request_id` is a server-generated UUID. Cache hits use `usage.cached=true` and report zero new tokens/cost. `usage_estimated=true` means the provider did not supply exact token counts. ```json { "request_id": "019b8df0-1234-4000-8000-000000000003", "tool": "detect", "result": { "text": "AI-like writing estimate: 50%\n\nAI detection is an uncalibrated estimate and may be wrong. Human writing can be flagged as AI, and AI writing can be missed. Never use this result alone to make decisions about a person.\n\n[50%] The research team spent several long months carefully studying how local people use public spaces in their neighborhood and listening to residents describe their daily routines.\n\n[50%] They found that shade, comfortable seating, and safe crossings mattered more to participants than expensive decorative features, so the final proposal focused on those practical needs.", "ai_estimate": 50, "method": "llm_heuristic", "disclaimer": "AI detection is an uncalibrated estimate and may be wrong. Human writing can be flagged as AI, and AI writing can be missed. Never use this result alone to make decisions about a person.", "sentences": [ { "text": "The research team spent several long months carefully studying how local people use public spaces in their neighborhood and listening to residents describe their daily routines.", "offset": 0, "length": 177, "ai_estimate": 50 }, { "text": "They found that shade, comfortable seating, and safe crossings mattered more to participants than expensive decorative features, so the final proposal focused on those practical needs.", "offset": 178, "length": 184, "ai_estimate": 50 } ] }, "usage": { "input_tokens": 140, "output_tokens": 80, "total_tokens": 220, "estimated_cost_usd": 0.000054, "usage_estimated": false, "cached": false, "provider": "primary", "model": "gpt-6-luna" }, "input_words": 52, "output_words": 93 } ``` ## Errors: cause and fix Every error uses this JSON shape and a link to its exact fix: ```json { "error": { "code": "unauthorized", "message": "A valid API key is required.", "docs_url": "https://metaphrase.msalman.com/docs/errors#unauthorized" } } ``` | Code | HTTP | Cause | Fix | | --- | --- | --- | --- | | [`invalid_request`](https://metaphrase.msalman.com/docs/errors#invalid_request) | 400 | Check the request fields and their allowed values. | Send the documented JSON fields with the correct types. Unknown fields are rejected. | | [`invalid_json`](https://metaphrase.msalman.com/docs/errors#invalid_json) | 400 | The request body is not valid JSON. | Serialize the request with JSON.stringify or your language's JSON encoder. | | [`unsupported_media_type`](https://metaphrase.msalman.com/docs/errors#unsupported_media_type) | 415 | Send application/json content. | Set Content-Type: application/json. File uploads use their separate documented route. | | [`body_too_large`](https://metaphrase.msalman.com/docs/errors#body_too_large) | 413 | The request body exceeds the allowed size. | Reduce the payload and split long documents at sentence boundaries. | | [`text_too_long`](https://metaphrase.msalman.com/docs/errors#text_too_long) | 413 | The text exceeds the configured character limit. | Use the max_input_chars value in the API specification and split longer documents. | | [`unauthorized`](https://metaphrase.msalman.com/docs/errors#unauthorized) | 401 | A valid API key is required. | Send Authorization: Bearer YOUR_KEY. Create a new key if the old one was revoked. | | [`forbidden`](https://metaphrase.msalman.com/docs/errors#forbidden) | 403 | This action is not available to this account. | Check account status and the key's access permissions. | | [`rate_limited`](https://metaphrase.msalman.com/docs/errors#rate_limited) | 429 | Too many requests. Please wait and try again. | Wait for Retry-After seconds and use exponential backoff with jitter. | | [`quota_exceeded`](https://metaphrase.msalman.com/docs/errors#quota_exceeded) | 429 | The monthly usage quota has been reached. | Wait for the next UTC calendar month or ask an administrator to change the plan. | | [`service_unavailable`](https://metaphrase.msalman.com/docs/errors#service_unavailable) | 503 | A required service is temporarily unavailable. | Wait and retry. The operator should check Redis, PostgreSQL, or the tool's health. | | [`not_found`](https://metaphrase.msalman.com/docs/errors#not_found) | 404 | This resource does not exist. | Check the API version, endpoint name, and resource identifier. | | [`method_not_allowed`](https://metaphrase.msalman.com/docs/errors#method_not_allowed) | 405 | This HTTP method is not supported. | Use POST for processing endpoints and the method documented for dashboard routes. | | [`internal_error`](https://metaphrase.msalman.com/docs/errors#internal_error) | 500 | The request could not be completed. | Retry once. If it persists, provide the X-Request-Id header to the operator. | | [`budget_exceeded`](https://metaphrase.msalman.com/docs/errors#budget_exceeded) | 429 | The writing service has reached its configured spending limit. | The operator must review usage and adjust the configured budget before more paid requests can run. | | [`provider_not_configured`](https://metaphrase.msalman.com/docs/errors#provider_not_configured) | 503 | The writing provider is not configured yet. | The operator must configure LLM_PRIMARY_API_KEY or a fallback provider in .env and recreate the web container. | | [`upstream_unavailable`](https://metaphrase.msalman.com/docs/errors#upstream_unavailable) | 503 | The writing provider is temporarily unavailable. | Retry with backoff. The operator can inspect provider availability without logging submitted text. | | [`upstream_timeout`](https://metaphrase.msalman.com/docs/errors#upstream_timeout) | 504 | The writing provider took too long to respond. | Retry a shorter input or wait before retrying. Do not append a retry to partial streamed output. | | [`upstream_error`](https://metaphrase.msalman.com/docs/errors#upstream_error) | 502 | The writing provider returned an invalid response. | Retry once with a shorter input. If it continues, the operator should check the configured model's compatibility. | | [`output_truncated`](https://metaphrase.msalman.com/docs/errors#output_truncated) | 502 | The response reached its output limit before finishing. | Use a shorter input or ask the operator to adjust the tool's output cap and chunk size. | | [`text_too_short`](https://metaphrase.msalman.com/docs/errors#text_too_short) | 422 | More text is needed for a meaningful estimate. | Submit at least the documented minimum word count. Very short texts cannot be assessed meaningfully. | For streaming requests, failures after HTTP headers are sent arrive as an SSE `error` event with this same error object. Never treat partial text as a completed result or append output from a retry. ## Rate limits and quotas Defaults: Free **15 requests per 60 seconds** and **500 accepted calls per key per UTC calendar month**; Pro placeholders **120/60 seconds** and **25,000/month**. The operator can change plans; response headers are authoritative. Calls to all tools share a key's limits. Accepted cache hits and upstream failures consume quota. Authentication/schema failures and provider-not-configured preflight failures do not. `X-RateLimit-Remaining`, `X-RateLimit-Reset`, `X-Quota-Remaining` and `X-Quota-Reset` show remaining capacity and Unix-second reset times. For HTTP 429 wait `Retry-After` seconds; quota/budget limits need a reset or operator action. Requests may also receive 429 when the shared processing slots are busy. See [retries and long texts](https://metaphrase.msalman.com/docs/reliability.md). ## Code samples These are complete backend programs, not frontend key examples. JavaScript fetch and Node.js are separate options; the latter uses the official SDK. Flutter UI must call your own backend; the Dart sample runs on a backend/CLI. See [mobile integration](https://metaphrase.msalman.com/docs/recipe-mobile.md). ### cURL Save as `request.sh`. Run: `bash request.sh`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```bash #!/usr/bin/env bash set -euo pipefail : "${METAPHRASE_API_KEY:?Set METAPHRASE_API_KEY in your backend environment}" BASE_URL="${METAPHRASE_BASE_URL:-https://metaphrase.msalman.com}" curl --fail-with-body --silent --show-error \ --request POST "${BASE_URL%/}/api/v1/detect" \ --header "Authorization: Bearer $METAPHRASE_API_KEY" \ --header 'Content-Type: application/json' \ --data-binary @- <<'JSON' { "text": "The research team spent several long months carefully studying how local people use public spaces in their neighborhood and listening to residents describe their daily routines. They found that shade, comfortable seating, and safe crossings mattered more to participants than expensive decorative features, so the final proposal focused on those practical needs.", "stream": false } JSON ``` ### JavaScript (fetch) Save as `request.mjs`. Run: `node request.mjs (Node.js 22+; backend only)`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```javascript const key = process.env.METAPHRASE_API_KEY; if (!key) throw new Error("Set METAPHRASE_API_KEY on your backend"); const base = (process.env.METAPHRASE_BASE_URL ?? "https://metaphrase.msalman.com").replace(/\/$/, ""); const payload = { "text": "The research team spent several long months carefully studying how local people use public spaces in their neighborhood and listening to residents describe their daily routines. They found that shade, comfortable seating, and safe crossings mattered more to participants than expensive decorative features, so the final proposal focused on those practical needs.", "stream": false }; const response = await fetch(base + "/api/v1/detect", { method: "POST", headers: { Authorization: "Bearer " + key, "Content-Type": "application/json" }, body: JSON.stringify(payload), signal: AbortSignal.timeout(250000), }); const result = await response.json(); if (!response.ok) { console.error(response.status, result.error.code, result.error.message, result.error.docs_url); process.exitCode = 1; } else { console.log(JSON.stringify(result, null, 2)); } ``` ### Node.js Save as `request-sdk.mjs`. Run: `npm install https://metaphrase.msalman.com/downloads/metaphrase-sdk-1.0.0.tgz && node request-sdk.mjs`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```javascript import { MetaPhrase, MetaPhraseError } from "@metaphrase/sdk"; const client = new MetaPhrase({ api_key: process.env.METAPHRASE_API_KEY, base_url: process.env.METAPHRASE_BASE_URL ?? "https://metaphrase.msalman.com", }); try { const result = await client.detect({ "text": "The research team spent several long months carefully studying how local people use public spaces in their neighborhood and listening to residents describe their daily routines. They found that shade, comfortable seating, and safe crossings mattered more to participants than expensive decorative features, so the final proposal focused on those practical needs.", "stream": false }); console.log(JSON.stringify(result, null, 2)); } catch (error) { if (error instanceof MetaPhraseError) console.error(error.status, error.code, error.message, error.docs_url); else console.error(error); process.exitCode = 1; } ``` ### Python Save as `request.py`. Run: `python3 request.py (Python 3.11+; standard library only)`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```python import json import os import sys from urllib.request import Request, urlopen from urllib.error import HTTPError key = os.environ["METAPHRASE_API_KEY"] base = os.environ.get("METAPHRASE_BASE_URL", "https://metaphrase.msalman.com").rstrip("/") payload = json.loads("{\"text\":\"The research team spent several long months carefully studying how local people use public spaces in their neighborhood and listening to residents describe their daily routines. They found that shade, comfortable seating, and safe crossings mattered more to participants than expensive decorative features, so the final proposal focused on those practical needs.\",\"stream\":false}") request = Request( base + "/api/v1/detect", data=json.dumps(payload, ensure_ascii=False).encode("utf-8"), headers={"Authorization": "Bearer " + key, "Content-Type": "application/json"}, method="POST", ) try: with urlopen(request, timeout=250) as response: print(json.dumps(json.load(response), ensure_ascii=False, indent=2)) except HTTPError as error: print(error.code, error.read().decode("utf-8"), file=sys.stderr) sys.exit(1) ``` ### PHP Save as `request.php`. Run: `php request.php (PHP 8.2+ with the cURL extension)`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 250, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key, 'Content-Type: application/json'], CURLOPT_POSTFIELDS => json_encode($payload, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE), ]); $body = curl_exec($curl); if ($body === false) { throw new RuntimeException(curl_error($curl)); } $status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE); curl_close($curl); $result = json_decode($body, true, 512, JSON_THROW_ON_ERROR); echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . PHP_EOL; if ($status >= 400) { exit(1); } ``` ### Java Save as `MetaPhraseExample.java`. Run: `javac -encoding UTF-8 MetaPhraseExample.java && java MetaPhraseExample (JDK 17+)`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Duration; public class MetaPhraseExample { public static void main(String[] args) throws Exception { String key = System.getenv("METAPHRASE_API_KEY"); if (key == null || key.isBlank()) throw new IllegalStateException("Set METAPHRASE_API_KEY"); String base = System.getenv().getOrDefault("METAPHRASE_BASE_URL", "https://metaphrase.msalman.com").replaceAll("/+$", ""); String payload = """ { "text": "The research team spent several long months carefully studying how local people use public spaces in their neighborhood and listening to residents describe their daily routines. They found that shade, comfortable seating, and safe crossings mattered more to participants than expensive decorative features, so the final proposal focused on those practical needs.", "stream": false } """; HttpRequest request = HttpRequest.newBuilder(URI.create(base + "/api/v1/detect")) .timeout(Duration.ofSeconds(250)) .header("Authorization", "Bearer " + key) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(payload)) .build(); HttpResponse response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); if (response.statusCode() >= 400) throw new java.io.IOException("HTTP " + response.statusCode() + ": " + response.body()); } } ``` ### C# Save as `Program.cs`. Run: `dotnet new console -n MetaPhraseExample; replace its Program.cs, then dotnet run --project MetaPhraseExample (.NET 8+)`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```csharp using System.Net.Http.Headers; using System.Text; string key = Environment.GetEnvironmentVariable("METAPHRASE_API_KEY") ?? throw new InvalidOperationException("Set METAPHRASE_API_KEY"); string baseUrl = (Environment.GetEnvironmentVariable("METAPHRASE_BASE_URL") ?? "https://metaphrase.msalman.com").TrimEnd('/'); string payload = """ { "text": "The research team spent several long months carefully studying how local people use public spaces in their neighborhood and listening to residents describe their daily routines. They found that shade, comfortable seating, and safe crossings mattered more to participants than expensive decorative features, so the final proposal focused on those practical needs.", "stream": false } """; using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(250) }; client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", key); using var content = new StringContent(payload, Encoding.UTF8, "application/json"); using var response = await client.PostAsync(baseUrl + "/api/v1/detect", content); string result = await response.Content.ReadAsStringAsync(); Console.WriteLine(result); if (!response.IsSuccessStatusCode) throw new HttpRequestException($"HTTP {(int)response.StatusCode}: {result}"); ``` ### Go Save as `main.go`. Run: `go run main.go (Go 1.22+; standard library only)`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```go package main import ( "bytes" "fmt" "io" "net/http" "os" "strings" "time" ) func main() { key := os.Getenv("METAPHRASE_API_KEY") if key == "" { panic("Set METAPHRASE_API_KEY") } base := os.Getenv("METAPHRASE_BASE_URL") if base == "" { base = "https://metaphrase.msalman.com" } payload := []byte(`{ "text": "The research team spent several long months carefully studying how local people use public spaces in their neighborhood and listening to residents describe their daily routines. They found that shade, comfortable seating, and safe crossings mattered more to participants than expensive decorative features, so the final proposal focused on those practical needs.", "stream": false }`) request, err := http.NewRequest(http.MethodPost, strings.TrimRight(base, "/") + "/api/v1/detect", bytes.NewReader(payload)) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + key) request.Header.Set("Content-Type", "application/json") client := &http.Client{Timeout: 250 * time.Second} response, err := client.Do(request) if err != nil { panic(err) } defer response.Body.Close() body, err := io.ReadAll(response.Body) if err != nil { panic(err) } fmt.Println(string(body)) if response.StatusCode >= 400 { os.Exit(1) } } ``` ### Flutter / Dart Save as `request.dart`. Run: `dart run request.dart (Dart 3+ backend/CLI; Flutter UI calls your backend proxy)`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```dart import 'dart:convert'; import 'dart:io'; Future main() async { final key = Platform.environment['METAPHRASE_API_KEY']; if (key == null || key.isEmpty) throw StateError('Set METAPHRASE_API_KEY on your backend'); final base = (Platform.environment['METAPHRASE_BASE_URL'] ?? 'https://metaphrase.msalman.com').replaceFirst(RegExp(r'/+$'), ''); final payload = jsonDecode(r'''{"text":"The research team spent several long months carefully studying how local people use public spaces in their neighborhood and listening to residents describe their daily routines. They found that shade, comfortable seating, and safe crossings mattered more to participants than expensive decorative features, so the final proposal focused on those practical needs.","stream":false}'''); final client = HttpClient()..connectionTimeout = const Duration(seconds: 30); try { final request = await client.postUrl(Uri.parse('$base/api/v1/detect')); request.headers.set(HttpHeaders.authorizationHeader, 'Bearer $key'); request.headers.contentType = ContentType.json; request.write(jsonEncode(payload)); final response = await request.close().timeout(const Duration(seconds: 250)); final body = await utf8.decoder.bind(response).join(); stdout.writeln(body); if (response.statusCode >= 400) exitCode = 1; } finally { client.close(force: true); } } ``` --- # translate Translate text across 42 languages with source detection and RTL metadata. ## Endpoint and method `POST https://metaphrase.msalman.com/api/v1/translate` ## Authentication Send `Authorization: Bearer ` and `Content-Type: application/json`. Create a key at [the dashboard](https://metaphrase.msalman.com/dashboard). Store it in backend environment variable `METAPHRASE_API_KEY`; never expose it in a browser, mobile binary, public repository, or widget. See [authentication and a working backend proxy](https://metaphrase.msalman.com/docs/authentication.md). ## Request parameters JSON uses snake_case. Unknown fields are rejected. Omit optional values instead of sending null. Maximum text length: **20,000 Unicode code points**. | Name | Type | Required | Default | Allowed values / range | Max length / items | | --- | --- | --- | --- | --- | --- | | `text` | string | Yes | — | — | 20000 | | `source_language` | string | No | "auto" | auto, en, ur, hi, ar, es, fr, de, pt, it, nl, pl, uk, ru, tr, fa, he, bn, pa, ta, te, mr, gu, ne, si, zh, zh-TW, ja, ko, id, ms, vi, th, tl, sv, da, no, fi, el, cs, ro, hu, sw | — | | `target_language` | string | Yes | — | en, ur, hi, ar, es, fr, de, pt, it, nl, pl, uk, ru, tr, fa, he, bn, pa, ta, te, mr, gu, ne, si, zh, zh-TW, ja, ko, id, ms, vi, th, tl, sv, da, no, fi, el, cs, ro, hu, sw | — | | `stream` | boolean | No | false | — | — | - **`text`**: Plain text. Limit counts Unicode code points. - **`source_language`**: Source language code or auto. Detection is heuristic and may be uncertain. - **`target_language`**: Required translation target language code. ur/ar/fa/he return direction=rtl. - **`stream`**: Use true for SSE on paraphrase/rewrite, translate or summarize. Other operations require false; alternatives always returns JSON. `source_language` is a local heuristic estimate for `auto`, or `und` if unknown. The writing provider also detects the source directly. For Urdu, Arabic, Persian and Hebrew use `dir="rtl"`; the response supplies `direction`. An explicitly identical source/target returns the input with zero model tokens. ## Full example request ```json { "text": "A good idea can come from anywhere.", "source_language": "auto", "target_language": "ur", "stream": false } ``` ## Full example response HTTP 200, `application/json`. This is an illustrative response; writing, source metadata, token counts and cost can vary. `request_id` is a server-generated UUID. Cache hits use `usage.cached=true` and report zero new tokens/cost. `usage_estimated=true` means the provider did not supply exact token counts. ```json { "request_id": "019b8df0-1234-4000-8000-000000000004", "tool": "translate", "result": { "text": "ایک اچھا خیال کہیں سے بھی آ سکتا ہے۔", "source_language": "en", "target_language": "ur", "source_language_detected": true, "direction": "rtl" }, "usage": { "input_tokens": 60, "output_tokens": 20, "total_tokens": 80, "estimated_cost_usd": 0.000016, "usage_estimated": false, "cached": false, "provider": "primary", "model": "gpt-6-luna" }, "input_words": 7, "output_words": 9 } ``` ## Errors: cause and fix Every error uses this JSON shape and a link to its exact fix: ```json { "error": { "code": "unauthorized", "message": "A valid API key is required.", "docs_url": "https://metaphrase.msalman.com/docs/errors#unauthorized" } } ``` | Code | HTTP | Cause | Fix | | --- | --- | --- | --- | | [`invalid_request`](https://metaphrase.msalman.com/docs/errors#invalid_request) | 400 | Check the request fields and their allowed values. | Send the documented JSON fields with the correct types. Unknown fields are rejected. | | [`invalid_json`](https://metaphrase.msalman.com/docs/errors#invalid_json) | 400 | The request body is not valid JSON. | Serialize the request with JSON.stringify or your language's JSON encoder. | | [`unsupported_media_type`](https://metaphrase.msalman.com/docs/errors#unsupported_media_type) | 415 | Send application/json content. | Set Content-Type: application/json. File uploads use their separate documented route. | | [`body_too_large`](https://metaphrase.msalman.com/docs/errors#body_too_large) | 413 | The request body exceeds the allowed size. | Reduce the payload and split long documents at sentence boundaries. | | [`text_too_long`](https://metaphrase.msalman.com/docs/errors#text_too_long) | 413 | The text exceeds the configured character limit. | Use the max_input_chars value in the API specification and split longer documents. | | [`unauthorized`](https://metaphrase.msalman.com/docs/errors#unauthorized) | 401 | A valid API key is required. | Send Authorization: Bearer YOUR_KEY. Create a new key if the old one was revoked. | | [`forbidden`](https://metaphrase.msalman.com/docs/errors#forbidden) | 403 | This action is not available to this account. | Check account status and the key's access permissions. | | [`rate_limited`](https://metaphrase.msalman.com/docs/errors#rate_limited) | 429 | Too many requests. Please wait and try again. | Wait for Retry-After seconds and use exponential backoff with jitter. | | [`quota_exceeded`](https://metaphrase.msalman.com/docs/errors#quota_exceeded) | 429 | The monthly usage quota has been reached. | Wait for the next UTC calendar month or ask an administrator to change the plan. | | [`service_unavailable`](https://metaphrase.msalman.com/docs/errors#service_unavailable) | 503 | A required service is temporarily unavailable. | Wait and retry. The operator should check Redis, PostgreSQL, or the tool's health. | | [`not_found`](https://metaphrase.msalman.com/docs/errors#not_found) | 404 | This resource does not exist. | Check the API version, endpoint name, and resource identifier. | | [`method_not_allowed`](https://metaphrase.msalman.com/docs/errors#method_not_allowed) | 405 | This HTTP method is not supported. | Use POST for processing endpoints and the method documented for dashboard routes. | | [`internal_error`](https://metaphrase.msalman.com/docs/errors#internal_error) | 500 | The request could not be completed. | Retry once. If it persists, provide the X-Request-Id header to the operator. | | [`budget_exceeded`](https://metaphrase.msalman.com/docs/errors#budget_exceeded) | 429 | The writing service has reached its configured spending limit. | The operator must review usage and adjust the configured budget before more paid requests can run. | | [`provider_not_configured`](https://metaphrase.msalman.com/docs/errors#provider_not_configured) | 503 | The writing provider is not configured yet. | The operator must configure LLM_PRIMARY_API_KEY or a fallback provider in .env and recreate the web container. | | [`upstream_unavailable`](https://metaphrase.msalman.com/docs/errors#upstream_unavailable) | 503 | The writing provider is temporarily unavailable. | Retry with backoff. The operator can inspect provider availability without logging submitted text. | | [`upstream_timeout`](https://metaphrase.msalman.com/docs/errors#upstream_timeout) | 504 | The writing provider took too long to respond. | Retry a shorter input or wait before retrying. Do not append a retry to partial streamed output. | | [`upstream_error`](https://metaphrase.msalman.com/docs/errors#upstream_error) | 502 | The writing provider returned an invalid response. | Retry once with a shorter input. If it continues, the operator should check the configured model's compatibility. | | [`output_truncated`](https://metaphrase.msalman.com/docs/errors#output_truncated) | 502 | The response reached its output limit before finishing. | Use a shorter input or ask the operator to adjust the tool's output cap and chunk size. | For streaming requests, failures after HTTP headers are sent arrive as an SSE `error` event with this same error object. Never treat partial text as a completed result or append output from a retry. ## Rate limits and quotas Defaults: Free **15 requests per 60 seconds** and **500 accepted calls per key per UTC calendar month**; Pro placeholders **120/60 seconds** and **25,000/month**. The operator can change plans; response headers are authoritative. Calls to all tools share a key's limits. Accepted cache hits and upstream failures consume quota. Authentication/schema failures and provider-not-configured preflight failures do not. `X-RateLimit-Remaining`, `X-RateLimit-Reset`, `X-Quota-Remaining` and `X-Quota-Reset` show remaining capacity and Unix-second reset times. For HTTP 429 wait `Retry-After` seconds; quota/budget limits need a reset or operator action. Requests may also receive 429 when the shared processing slots are busy. See [retries and long texts](https://metaphrase.msalman.com/docs/reliability.md). ## Code samples These are complete backend programs, not frontend key examples. JavaScript fetch and Node.js are separate options; the latter uses the official SDK. Flutter UI must call your own backend; the Dart sample runs on a backend/CLI. See [mobile integration](https://metaphrase.msalman.com/docs/recipe-mobile.md). ### cURL Save as `request.sh`. Run: `bash request.sh`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```bash #!/usr/bin/env bash set -euo pipefail : "${METAPHRASE_API_KEY:?Set METAPHRASE_API_KEY in your backend environment}" BASE_URL="${METAPHRASE_BASE_URL:-https://metaphrase.msalman.com}" curl --fail-with-body --silent --show-error \ --request POST "${BASE_URL%/}/api/v1/translate" \ --header "Authorization: Bearer $METAPHRASE_API_KEY" \ --header 'Content-Type: application/json' \ --data-binary @- <<'JSON' { "text": "A good idea can come from anywhere.", "source_language": "auto", "target_language": "ur", "stream": false } JSON ``` ### JavaScript (fetch) Save as `request.mjs`. Run: `node request.mjs (Node.js 22+; backend only)`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```javascript const key = process.env.METAPHRASE_API_KEY; if (!key) throw new Error("Set METAPHRASE_API_KEY on your backend"); const base = (process.env.METAPHRASE_BASE_URL ?? "https://metaphrase.msalman.com").replace(/\/$/, ""); const payload = { "text": "A good idea can come from anywhere.", "source_language": "auto", "target_language": "ur", "stream": false }; const response = await fetch(base + "/api/v1/translate", { method: "POST", headers: { Authorization: "Bearer " + key, "Content-Type": "application/json" }, body: JSON.stringify(payload), signal: AbortSignal.timeout(250000), }); const result = await response.json(); if (!response.ok) { console.error(response.status, result.error.code, result.error.message, result.error.docs_url); process.exitCode = 1; } else { console.log(JSON.stringify(result, null, 2)); } ``` ### Node.js Save as `request-sdk.mjs`. Run: `npm install https://metaphrase.msalman.com/downloads/metaphrase-sdk-1.0.0.tgz && node request-sdk.mjs`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```javascript import { MetaPhrase, MetaPhraseError } from "@metaphrase/sdk"; const client = new MetaPhrase({ api_key: process.env.METAPHRASE_API_KEY, base_url: process.env.METAPHRASE_BASE_URL ?? "https://metaphrase.msalman.com", }); try { const result = await client.translate({ "text": "A good idea can come from anywhere.", "source_language": "auto", "target_language": "ur", "stream": false }); console.log(JSON.stringify(result, null, 2)); } catch (error) { if (error instanceof MetaPhraseError) console.error(error.status, error.code, error.message, error.docs_url); else console.error(error); process.exitCode = 1; } ``` ### Python Save as `request.py`. Run: `python3 request.py (Python 3.11+; standard library only)`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```python import json import os import sys from urllib.request import Request, urlopen from urllib.error import HTTPError key = os.environ["METAPHRASE_API_KEY"] base = os.environ.get("METAPHRASE_BASE_URL", "https://metaphrase.msalman.com").rstrip("/") payload = json.loads("{\"text\":\"A good idea can come from anywhere.\",\"source_language\":\"auto\",\"target_language\":\"ur\",\"stream\":false}") request = Request( base + "/api/v1/translate", data=json.dumps(payload, ensure_ascii=False).encode("utf-8"), headers={"Authorization": "Bearer " + key, "Content-Type": "application/json"}, method="POST", ) try: with urlopen(request, timeout=250) as response: print(json.dumps(json.load(response), ensure_ascii=False, indent=2)) except HTTPError as error: print(error.code, error.read().decode("utf-8"), file=sys.stderr) sys.exit(1) ``` ### PHP Save as `request.php`. Run: `php request.php (PHP 8.2+ with the cURL extension)`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 250, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key, 'Content-Type: application/json'], CURLOPT_POSTFIELDS => json_encode($payload, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE), ]); $body = curl_exec($curl); if ($body === false) { throw new RuntimeException(curl_error($curl)); } $status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE); curl_close($curl); $result = json_decode($body, true, 512, JSON_THROW_ON_ERROR); echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . PHP_EOL; if ($status >= 400) { exit(1); } ``` ### Java Save as `MetaPhraseExample.java`. Run: `javac -encoding UTF-8 MetaPhraseExample.java && java MetaPhraseExample (JDK 17+)`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Duration; public class MetaPhraseExample { public static void main(String[] args) throws Exception { String key = System.getenv("METAPHRASE_API_KEY"); if (key == null || key.isBlank()) throw new IllegalStateException("Set METAPHRASE_API_KEY"); String base = System.getenv().getOrDefault("METAPHRASE_BASE_URL", "https://metaphrase.msalman.com").replaceAll("/+$", ""); String payload = """ { "text": "A good idea can come from anywhere.", "source_language": "auto", "target_language": "ur", "stream": false } """; HttpRequest request = HttpRequest.newBuilder(URI.create(base + "/api/v1/translate")) .timeout(Duration.ofSeconds(250)) .header("Authorization", "Bearer " + key) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(payload)) .build(); HttpResponse response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); if (response.statusCode() >= 400) throw new java.io.IOException("HTTP " + response.statusCode() + ": " + response.body()); } } ``` ### C# Save as `Program.cs`. Run: `dotnet new console -n MetaPhraseExample; replace its Program.cs, then dotnet run --project MetaPhraseExample (.NET 8+)`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```csharp using System.Net.Http.Headers; using System.Text; string key = Environment.GetEnvironmentVariable("METAPHRASE_API_KEY") ?? throw new InvalidOperationException("Set METAPHRASE_API_KEY"); string baseUrl = (Environment.GetEnvironmentVariable("METAPHRASE_BASE_URL") ?? "https://metaphrase.msalman.com").TrimEnd('/'); string payload = """ { "text": "A good idea can come from anywhere.", "source_language": "auto", "target_language": "ur", "stream": false } """; using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(250) }; client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", key); using var content = new StringContent(payload, Encoding.UTF8, "application/json"); using var response = await client.PostAsync(baseUrl + "/api/v1/translate", content); string result = await response.Content.ReadAsStringAsync(); Console.WriteLine(result); if (!response.IsSuccessStatusCode) throw new HttpRequestException($"HTTP {(int)response.StatusCode}: {result}"); ``` ### Go Save as `main.go`. Run: `go run main.go (Go 1.22+; standard library only)`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```go package main import ( "bytes" "fmt" "io" "net/http" "os" "strings" "time" ) func main() { key := os.Getenv("METAPHRASE_API_KEY") if key == "" { panic("Set METAPHRASE_API_KEY") } base := os.Getenv("METAPHRASE_BASE_URL") if base == "" { base = "https://metaphrase.msalman.com" } payload := []byte(`{ "text": "A good idea can come from anywhere.", "source_language": "auto", "target_language": "ur", "stream": false }`) request, err := http.NewRequest(http.MethodPost, strings.TrimRight(base, "/") + "/api/v1/translate", bytes.NewReader(payload)) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + key) request.Header.Set("Content-Type", "application/json") client := &http.Client{Timeout: 250 * time.Second} response, err := client.Do(request) if err != nil { panic(err) } defer response.Body.Close() body, err := io.ReadAll(response.Body) if err != nil { panic(err) } fmt.Println(string(body)) if response.StatusCode >= 400 { os.Exit(1) } } ``` ### Flutter / Dart Save as `request.dart`. Run: `dart run request.dart (Dart 3+ backend/CLI; Flutter UI calls your backend proxy)`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```dart import 'dart:convert'; import 'dart:io'; Future main() async { final key = Platform.environment['METAPHRASE_API_KEY']; if (key == null || key.isEmpty) throw StateError('Set METAPHRASE_API_KEY on your backend'); final base = (Platform.environment['METAPHRASE_BASE_URL'] ?? 'https://metaphrase.msalman.com').replaceFirst(RegExp(r'/+$'), ''); final payload = jsonDecode(r'''{"text":"A good idea can come from anywhere.","source_language":"auto","target_language":"ur","stream":false}'''); final client = HttpClient()..connectionTimeout = const Duration(seconds: 30); try { final request = await client.postUrl(Uri.parse('$base/api/v1/translate')); request.headers.set(HttpHeaders.authorizationHeader, 'Bearer $key'); request.headers.contentType = ContentType.json; request.write(jsonEncode(payload)); final response = await request.close().timeout(const Duration(seconds: 250)); final body = await utf8.decoder.bind(response).join(); stdout.writeln(body); if (response.statusCode >= 400) exitCode = 1; } finally { client.close(force: true); } } ``` --- # summarize Create paragraphs, bullets, or exact source key sentences. ## Endpoint and method `POST https://metaphrase.msalman.com/api/v1/summarize` ## Authentication Send `Authorization: Bearer ` and `Content-Type: application/json`. Create a key at [the dashboard](https://metaphrase.msalman.com/dashboard). Store it in backend environment variable `METAPHRASE_API_KEY`; never expose it in a browser, mobile binary, public repository, or widget. See [authentication and a working backend proxy](https://metaphrase.msalman.com/docs/authentication.md). ## Request parameters JSON uses snake_case. Unknown fields are rejected. Omit optional values instead of sending null. Maximum text length: **20,000 Unicode code points**. | Name | Type | Required | Default | Allowed values / range | Max length / items | | --- | --- | --- | --- | --- | --- | | `text` | string | Yes | — | — | 20000 | | `format` | string | No | "paragraph" | paragraph, bullets, key_sentences | — | | `length` | integer | No | 30 | 10 to 70 | — | | `stream` | boolean | No | false | — | — | - **`text`**: Plain text. Limit counts Unicode code points. - **`format`**: paragraph or bullets use an LLM; key_sentences selects exact source sentences locally without an LLM. - **`length`**: Approximate summary length as a percentage of source words. Whole sentences may exceed the target. - **`stream`**: Use true for SSE on paraphrase/rewrite, translate or summarize. Other operations require false; alternatives always returns JSON. `length` targets 10–70% of source words and is approximate. Whole sentences can exceed the target. `key_sentences` runs locally, keeps exact source sentence text and UTF-16 offsets, and works without an LLM. Paragraph/bullet modes use sequential bounded provider chunks and can summarize sections separately. ## Full example request ```json { "text": "Solar panels turn sunlight into electricity. Batteries store the energy for use at night. Regular maintenance keeps the system working efficiently.", "format": "key_sentences", "length": 30, "stream": false } ``` ## Full example response HTTP 200, `application/json`. This is an illustrative response; writing, source metadata, token counts and cost can vary. `request_id` is a server-generated UUID. Cache hits use `usage.cached=true` and report zero new tokens/cost. `usage_estimated=true` means the provider did not supply exact token counts. ```json { "request_id": "019b8df0-1234-4000-8000-000000000005", "tool": "summarize", "result": { "text": "Solar panels turn sunlight into electricity.", "format": "key_sentences", "length": 30, "sentences": [ { "text": "Solar panels turn sunlight into electricity. ", "offset": 0, "length": 45 } ] }, "usage": { "input_tokens": 0, "output_tokens": 0, "total_tokens": 0, "estimated_cost_usd": 0, "usage_estimated": false, "cached": false, "provider": "self_hosted", "model": "extractive_tfidf_v1" }, "input_words": 21, "output_words": 6 } ``` ## Errors: cause and fix Every error uses this JSON shape and a link to its exact fix: ```json { "error": { "code": "unauthorized", "message": "A valid API key is required.", "docs_url": "https://metaphrase.msalman.com/docs/errors#unauthorized" } } ``` | Code | HTTP | Cause | Fix | | --- | --- | --- | --- | | [`invalid_request`](https://metaphrase.msalman.com/docs/errors#invalid_request) | 400 | Check the request fields and their allowed values. | Send the documented JSON fields with the correct types. Unknown fields are rejected. | | [`invalid_json`](https://metaphrase.msalman.com/docs/errors#invalid_json) | 400 | The request body is not valid JSON. | Serialize the request with JSON.stringify or your language's JSON encoder. | | [`unsupported_media_type`](https://metaphrase.msalman.com/docs/errors#unsupported_media_type) | 415 | Send application/json content. | Set Content-Type: application/json. File uploads use their separate documented route. | | [`body_too_large`](https://metaphrase.msalman.com/docs/errors#body_too_large) | 413 | The request body exceeds the allowed size. | Reduce the payload and split long documents at sentence boundaries. | | [`text_too_long`](https://metaphrase.msalman.com/docs/errors#text_too_long) | 413 | The text exceeds the configured character limit. | Use the max_input_chars value in the API specification and split longer documents. | | [`unauthorized`](https://metaphrase.msalman.com/docs/errors#unauthorized) | 401 | A valid API key is required. | Send Authorization: Bearer YOUR_KEY. Create a new key if the old one was revoked. | | [`forbidden`](https://metaphrase.msalman.com/docs/errors#forbidden) | 403 | This action is not available to this account. | Check account status and the key's access permissions. | | [`rate_limited`](https://metaphrase.msalman.com/docs/errors#rate_limited) | 429 | Too many requests. Please wait and try again. | Wait for Retry-After seconds and use exponential backoff with jitter. | | [`quota_exceeded`](https://metaphrase.msalman.com/docs/errors#quota_exceeded) | 429 | The monthly usage quota has been reached. | Wait for the next UTC calendar month or ask an administrator to change the plan. | | [`service_unavailable`](https://metaphrase.msalman.com/docs/errors#service_unavailable) | 503 | A required service is temporarily unavailable. | Wait and retry. The operator should check Redis, PostgreSQL, or the tool's health. | | [`not_found`](https://metaphrase.msalman.com/docs/errors#not_found) | 404 | This resource does not exist. | Check the API version, endpoint name, and resource identifier. | | [`method_not_allowed`](https://metaphrase.msalman.com/docs/errors#method_not_allowed) | 405 | This HTTP method is not supported. | Use POST for processing endpoints and the method documented for dashboard routes. | | [`internal_error`](https://metaphrase.msalman.com/docs/errors#internal_error) | 500 | The request could not be completed. | Retry once. If it persists, provide the X-Request-Id header to the operator. | | [`budget_exceeded`](https://metaphrase.msalman.com/docs/errors#budget_exceeded) | 429 | The writing service has reached its configured spending limit. | The operator must review usage and adjust the configured budget before more paid requests can run. | | [`provider_not_configured`](https://metaphrase.msalman.com/docs/errors#provider_not_configured) | 503 | The writing provider is not configured yet. | The operator must configure LLM_PRIMARY_API_KEY or a fallback provider in .env and recreate the web container. | | [`upstream_unavailable`](https://metaphrase.msalman.com/docs/errors#upstream_unavailable) | 503 | The writing provider is temporarily unavailable. | Retry with backoff. The operator can inspect provider availability without logging submitted text. | | [`upstream_timeout`](https://metaphrase.msalman.com/docs/errors#upstream_timeout) | 504 | The writing provider took too long to respond. | Retry a shorter input or wait before retrying. Do not append a retry to partial streamed output. | | [`upstream_error`](https://metaphrase.msalman.com/docs/errors#upstream_error) | 502 | The writing provider returned an invalid response. | Retry once with a shorter input. If it continues, the operator should check the configured model's compatibility. | | [`output_truncated`](https://metaphrase.msalman.com/docs/errors#output_truncated) | 502 | The response reached its output limit before finishing. | Use a shorter input or ask the operator to adjust the tool's output cap and chunk size. | For streaming requests, failures after HTTP headers are sent arrive as an SSE `error` event with this same error object. Never treat partial text as a completed result or append output from a retry. ## Rate limits and quotas Defaults: Free **15 requests per 60 seconds** and **500 accepted calls per key per UTC calendar month**; Pro placeholders **120/60 seconds** and **25,000/month**. The operator can change plans; response headers are authoritative. Calls to all tools share a key's limits. Accepted cache hits and upstream failures consume quota. Authentication/schema failures and provider-not-configured preflight failures do not. `X-RateLimit-Remaining`, `X-RateLimit-Reset`, `X-Quota-Remaining` and `X-Quota-Reset` show remaining capacity and Unix-second reset times. For HTTP 429 wait `Retry-After` seconds; quota/budget limits need a reset or operator action. Requests may also receive 429 when the shared processing slots are busy. See [retries and long texts](https://metaphrase.msalman.com/docs/reliability.md). ## Code samples These are complete backend programs, not frontend key examples. JavaScript fetch and Node.js are separate options; the latter uses the official SDK. Flutter UI must call your own backend; the Dart sample runs on a backend/CLI. See [mobile integration](https://metaphrase.msalman.com/docs/recipe-mobile.md). ### cURL Save as `request.sh`. Run: `bash request.sh`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```bash #!/usr/bin/env bash set -euo pipefail : "${METAPHRASE_API_KEY:?Set METAPHRASE_API_KEY in your backend environment}" BASE_URL="${METAPHRASE_BASE_URL:-https://metaphrase.msalman.com}" curl --fail-with-body --silent --show-error \ --request POST "${BASE_URL%/}/api/v1/summarize" \ --header "Authorization: Bearer $METAPHRASE_API_KEY" \ --header 'Content-Type: application/json' \ --data-binary @- <<'JSON' { "text": "Solar panels turn sunlight into electricity. Batteries store the energy for use at night. Regular maintenance keeps the system working efficiently.", "format": "key_sentences", "length": 30, "stream": false } JSON ``` ### JavaScript (fetch) Save as `request.mjs`. Run: `node request.mjs (Node.js 22+; backend only)`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```javascript const key = process.env.METAPHRASE_API_KEY; if (!key) throw new Error("Set METAPHRASE_API_KEY on your backend"); const base = (process.env.METAPHRASE_BASE_URL ?? "https://metaphrase.msalman.com").replace(/\/$/, ""); const payload = { "text": "Solar panels turn sunlight into electricity. Batteries store the energy for use at night. Regular maintenance keeps the system working efficiently.", "format": "key_sentences", "length": 30, "stream": false }; const response = await fetch(base + "/api/v1/summarize", { method: "POST", headers: { Authorization: "Bearer " + key, "Content-Type": "application/json" }, body: JSON.stringify(payload), signal: AbortSignal.timeout(250000), }); const result = await response.json(); if (!response.ok) { console.error(response.status, result.error.code, result.error.message, result.error.docs_url); process.exitCode = 1; } else { console.log(JSON.stringify(result, null, 2)); } ``` ### Node.js Save as `request-sdk.mjs`. Run: `npm install https://metaphrase.msalman.com/downloads/metaphrase-sdk-1.0.0.tgz && node request-sdk.mjs`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```javascript import { MetaPhrase, MetaPhraseError } from "@metaphrase/sdk"; const client = new MetaPhrase({ api_key: process.env.METAPHRASE_API_KEY, base_url: process.env.METAPHRASE_BASE_URL ?? "https://metaphrase.msalman.com", }); try { const result = await client.summarize({ "text": "Solar panels turn sunlight into electricity. Batteries store the energy for use at night. Regular maintenance keeps the system working efficiently.", "format": "key_sentences", "length": 30, "stream": false }); console.log(JSON.stringify(result, null, 2)); } catch (error) { if (error instanceof MetaPhraseError) console.error(error.status, error.code, error.message, error.docs_url); else console.error(error); process.exitCode = 1; } ``` ### Python Save as `request.py`. Run: `python3 request.py (Python 3.11+; standard library only)`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```python import json import os import sys from urllib.request import Request, urlopen from urllib.error import HTTPError key = os.environ["METAPHRASE_API_KEY"] base = os.environ.get("METAPHRASE_BASE_URL", "https://metaphrase.msalman.com").rstrip("/") payload = json.loads("{\"text\":\"Solar panels turn sunlight into electricity. Batteries store the energy for use at night. Regular maintenance keeps the system working efficiently.\",\"format\":\"key_sentences\",\"length\":30,\"stream\":false}") request = Request( base + "/api/v1/summarize", data=json.dumps(payload, ensure_ascii=False).encode("utf-8"), headers={"Authorization": "Bearer " + key, "Content-Type": "application/json"}, method="POST", ) try: with urlopen(request, timeout=250) as response: print(json.dumps(json.load(response), ensure_ascii=False, indent=2)) except HTTPError as error: print(error.code, error.read().decode("utf-8"), file=sys.stderr) sys.exit(1) ``` ### PHP Save as `request.php`. Run: `php request.php (PHP 8.2+ with the cURL extension)`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 250, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key, 'Content-Type: application/json'], CURLOPT_POSTFIELDS => json_encode($payload, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE), ]); $body = curl_exec($curl); if ($body === false) { throw new RuntimeException(curl_error($curl)); } $status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE); curl_close($curl); $result = json_decode($body, true, 512, JSON_THROW_ON_ERROR); echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . PHP_EOL; if ($status >= 400) { exit(1); } ``` ### Java Save as `MetaPhraseExample.java`. Run: `javac -encoding UTF-8 MetaPhraseExample.java && java MetaPhraseExample (JDK 17+)`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Duration; public class MetaPhraseExample { public static void main(String[] args) throws Exception { String key = System.getenv("METAPHRASE_API_KEY"); if (key == null || key.isBlank()) throw new IllegalStateException("Set METAPHRASE_API_KEY"); String base = System.getenv().getOrDefault("METAPHRASE_BASE_URL", "https://metaphrase.msalman.com").replaceAll("/+$", ""); String payload = """ { "text": "Solar panels turn sunlight into electricity. Batteries store the energy for use at night. Regular maintenance keeps the system working efficiently.", "format": "key_sentences", "length": 30, "stream": false } """; HttpRequest request = HttpRequest.newBuilder(URI.create(base + "/api/v1/summarize")) .timeout(Duration.ofSeconds(250)) .header("Authorization", "Bearer " + key) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(payload)) .build(); HttpResponse response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); if (response.statusCode() >= 400) throw new java.io.IOException("HTTP " + response.statusCode() + ": " + response.body()); } } ``` ### C# Save as `Program.cs`. Run: `dotnet new console -n MetaPhraseExample; replace its Program.cs, then dotnet run --project MetaPhraseExample (.NET 8+)`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```csharp using System.Net.Http.Headers; using System.Text; string key = Environment.GetEnvironmentVariable("METAPHRASE_API_KEY") ?? throw new InvalidOperationException("Set METAPHRASE_API_KEY"); string baseUrl = (Environment.GetEnvironmentVariable("METAPHRASE_BASE_URL") ?? "https://metaphrase.msalman.com").TrimEnd('/'); string payload = """ { "text": "Solar panels turn sunlight into electricity. Batteries store the energy for use at night. Regular maintenance keeps the system working efficiently.", "format": "key_sentences", "length": 30, "stream": false } """; using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(250) }; client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", key); using var content = new StringContent(payload, Encoding.UTF8, "application/json"); using var response = await client.PostAsync(baseUrl + "/api/v1/summarize", content); string result = await response.Content.ReadAsStringAsync(); Console.WriteLine(result); if (!response.IsSuccessStatusCode) throw new HttpRequestException($"HTTP {(int)response.StatusCode}: {result}"); ``` ### Go Save as `main.go`. Run: `go run main.go (Go 1.22+; standard library only)`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```go package main import ( "bytes" "fmt" "io" "net/http" "os" "strings" "time" ) func main() { key := os.Getenv("METAPHRASE_API_KEY") if key == "" { panic("Set METAPHRASE_API_KEY") } base := os.Getenv("METAPHRASE_BASE_URL") if base == "" { base = "https://metaphrase.msalman.com" } payload := []byte(`{ "text": "Solar panels turn sunlight into electricity. Batteries store the energy for use at night. Regular maintenance keeps the system working efficiently.", "format": "key_sentences", "length": 30, "stream": false }`) request, err := http.NewRequest(http.MethodPost, strings.TrimRight(base, "/") + "/api/v1/summarize", bytes.NewReader(payload)) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + key) request.Header.Set("Content-Type", "application/json") client := &http.Client{Timeout: 250 * time.Second} response, err := client.Do(request) if err != nil { panic(err) } defer response.Body.Close() body, err := io.ReadAll(response.Body) if err != nil { panic(err) } fmt.Println(string(body)) if response.StatusCode >= 400 { os.Exit(1) } } ``` ### Flutter / Dart Save as `request.dart`. Run: `dart run request.dart (Dart 3+ backend/CLI; Flutter UI calls your backend proxy)`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```dart import 'dart:convert'; import 'dart:io'; Future main() async { final key = Platform.environment['METAPHRASE_API_KEY']; if (key == null || key.isEmpty) throw StateError('Set METAPHRASE_API_KEY on your backend'); final base = (Platform.environment['METAPHRASE_BASE_URL'] ?? 'https://metaphrase.msalman.com').replaceFirst(RegExp(r'/+$'), ''); final payload = jsonDecode(r'''{"text":"Solar panels turn sunlight into electricity. Batteries store the energy for use at night. Regular maintenance keeps the system working efficiently.","format":"key_sentences","length":30,"stream":false}'''); final client = HttpClient()..connectionTimeout = const Duration(seconds: 30); try { final request = await client.postUrl(Uri.parse('$base/api/v1/summarize')); request.headers.set(HttpHeaders.authorizationHeader, 'Bearer $key'); request.headers.contentType = ContentType.json; request.write(jsonEncode(payload)); final response = await request.close().timeout(const Duration(seconds: 250)); final body = await utf8.decoder.bind(response).join(); stdout.writeln(body); if (response.statusCode >= 400) exitCode = 1; } finally { client.close(force: true); } } ``` --- # cite Generate a citation and BibTeX/RIS exports from source metadata, without an LLM. ## Endpoint and method `POST https://metaphrase.msalman.com/api/v1/cite` ## Authentication Send `Authorization: Bearer ` and `Content-Type: application/json`. Create a key at [the dashboard](https://metaphrase.msalman.com/dashboard). Store it in backend environment variable `METAPHRASE_API_KEY`; never expose it in a browser, mobile binary, public repository, or widget. See [authentication and a working backend proxy](https://metaphrase.msalman.com/docs/authentication.md). ## Request parameters JSON uses snake_case. Unknown fields are rejected. Omit optional values instead of sending null. Maximum text length: **20,000 Unicode code points**. | Name | Type | Required | Default | Allowed values / range | Max length / items | | --- | --- | --- | --- | --- | --- | | `source_type` | string | No | "auto" | auto, url, doi, isbn, manual | — | | `source` | string | Unless source_type=manual | — | — | 2048 | | `style` | string | No | "apa" | apa, mla, chicago, harvard, ieee | — | | `fields` | object | If source_type=manual | — | — | — | | `fields.type` | string | No | "webpage" | webpage, book, article-journal | — | | `fields.title` | string | Yes | — | — | 500 | | `fields.authors` | array | No | [] | — | 50 | | `fields.authors[].given` | string | No | — | — | 120 | | `fields.authors[].family` | string | No | — | — | 120 | | `fields.authors[].literal` | string | No | — | — | 250 | | `fields.year` | integer | No | — | 1 to 9999 | — | | `fields.month` | integer | No | — | 1 to 12 | — | | `fields.day` | integer | No | — | 1 to 31 | — | | `fields.url` | string | No | — | uri | 2048 | | `fields.publisher` | string | No | — | — | 250 | | `fields.container_title` | string | No | — | — | 300 | | `fields.volume` | string | No | — | — | 40 | | `fields.issue` | string | No | — | — | 40 | | `fields.pages` | string | No | — | — | 80 | | `fields.doi` | string | No | — | — | 255 | | `fields.isbn` | string | No | — | — | 25 | | `fields.accessed_date` | string | No | — | date | — | | `stream` | boolean | No | false | false | — | - **`source_type`**: auto recognizes DOI, public HTTP(S) URL or ISBN; manual requires fields instead of source. - **`source`**: A DOI (plain or doi.org URL), a checksum-valid ISBN-10/13, or a public HTTP(S) page URL. Private networks, credentials and nonstandard ports are rejected. - **`style`**: apa=APA 7; mla=MLA 9; chicago=Chicago 18 author-date; harvard=Cite Them Right 12; ieee=IEEE. - **`fields`**: Manual source metadata. Required only for source_type=manual; title is required. Other missing metadata is reported in warnings. - **`stream`**: Use true for SSE on paraphrase/rewrite, translate or summarize. Other operations require false; alternatives always returns JSON. Use `fields` only with `source_type=manual`; `fields.title` is required. Every author needs `family` or `literal`; `month` requires `year`, and `day` requires a valid month/year date. Omit unknown values instead of sending null. `auto` recognizes DOI URLs/plain DOI, HTTP(S) URL, or checksum-valid ISBN. URL metadata reads only public HTTP(S) pages on ports 80/443, checks every redirect and pins validated DNS. Use manual fields for blocked, scanned, missing or incomplete metadata. Styles: APA 7, MLA 9, Chicago 18 author-date, Harvard Cite Them Right 12 and IEEE. Copy `bibtex` into a `.bib` file or `ris` into a `.ris` file. ## Full example request ```json { "source_type": "manual", "style": "apa", "fields": { "type": "book", "title": "A clear idea", "authors": [ { "given": "Ada", "family": "Lovelace" } ], "year": 2024, "publisher": "Example Press" }, "stream": false } ``` ## Full example response HTTP 200, `application/json`. This is an illustrative response; writing, source metadata, token counts and cost can vary. `request_id` is a server-generated UUID. Cache hits use `usage.cached=true` and report zero new tokens/cost. `usage_estimated=true` means the provider did not supply exact token counts. ```json { "request_id": "019b8df0-1234-4000-8000-000000000006", "tool": "cite", "result": { "text": "Lovelace, A. (2024). A clear idea. Example Press.", "style": "apa", "style_name": "APA 7", "source_type": "manual", "metadata": { "type": "book", "title": "A clear idea", "authors": [ { "given": "Ada", "family": "Lovelace" } ], "year": 2024, "publisher": "Example Press" }, "bibtex": "@book{mpfda3a81f9b10,\n author = {Lovelace, Ada},\n title = {A clear idea},\n year = {2024},\n publisher = {Example Press}\n}\n", "ris": "TY - BOOK\nAU - Lovelace, Ada\nTI - A clear idea\nPY - 2024\nPB - Example Press\nER -\n", "warnings": [] }, "usage": { "input_tokens": 0, "output_tokens": 0, "total_tokens": 0, "estimated_cost_usd": 0, "usage_estimated": false, "cached": false, "provider": "self_hosted", "model": "citation.js_0.9.0" }, "input_words": 3, "output_words": 8 } ``` ## Errors: cause and fix Every error uses this JSON shape and a link to its exact fix: ```json { "error": { "code": "unauthorized", "message": "A valid API key is required.", "docs_url": "https://metaphrase.msalman.com/docs/errors#unauthorized" } } ``` | Code | HTTP | Cause | Fix | | --- | --- | --- | --- | | [`invalid_request`](https://metaphrase.msalman.com/docs/errors#invalid_request) | 400 | Check the request fields and their allowed values. | Send the documented JSON fields with the correct types. Unknown fields are rejected. | | [`invalid_json`](https://metaphrase.msalman.com/docs/errors#invalid_json) | 400 | The request body is not valid JSON. | Serialize the request with JSON.stringify or your language's JSON encoder. | | [`unsupported_media_type`](https://metaphrase.msalman.com/docs/errors#unsupported_media_type) | 415 | Send application/json content. | Set Content-Type: application/json. File uploads use their separate documented route. | | [`body_too_large`](https://metaphrase.msalman.com/docs/errors#body_too_large) | 413 | The request body exceeds the allowed size. | Reduce the payload and split long documents at sentence boundaries. | | [`text_too_long`](https://metaphrase.msalman.com/docs/errors#text_too_long) | 413 | The text exceeds the configured character limit. | Use the max_input_chars value in the API specification and split longer documents. | | [`unauthorized`](https://metaphrase.msalman.com/docs/errors#unauthorized) | 401 | A valid API key is required. | Send Authorization: Bearer YOUR_KEY. Create a new key if the old one was revoked. | | [`forbidden`](https://metaphrase.msalman.com/docs/errors#forbidden) | 403 | This action is not available to this account. | Check account status and the key's access permissions. | | [`rate_limited`](https://metaphrase.msalman.com/docs/errors#rate_limited) | 429 | Too many requests. Please wait and try again. | Wait for Retry-After seconds and use exponential backoff with jitter. | | [`quota_exceeded`](https://metaphrase.msalman.com/docs/errors#quota_exceeded) | 429 | The monthly usage quota has been reached. | Wait for the next UTC calendar month or ask an administrator to change the plan. | | [`service_unavailable`](https://metaphrase.msalman.com/docs/errors#service_unavailable) | 503 | A required service is temporarily unavailable. | Wait and retry. The operator should check Redis, PostgreSQL, or the tool's health. | | [`not_found`](https://metaphrase.msalman.com/docs/errors#not_found) | 404 | This resource does not exist. | Check the API version, endpoint name, and resource identifier. | | [`method_not_allowed`](https://metaphrase.msalman.com/docs/errors#method_not_allowed) | 405 | This HTTP method is not supported. | Use POST for processing endpoints and the method documented for dashboard routes. | | [`internal_error`](https://metaphrase.msalman.com/docs/errors#internal_error) | 500 | The request could not be completed. | Retry once. If it persists, provide the X-Request-Id header to the operator. | | [`invalid_source`](https://metaphrase.msalman.com/docs/errors#invalid_source) | 422 | The source could not be used. | Check the DOI, ISBN, or public HTTP URL, or use manual citation fields. | | [`source_unavailable`](https://metaphrase.msalman.com/docs/errors#source_unavailable) | 422 | Source metadata could not be retrieved. | Try a DOI or ISBN, or enter the source details manually. | For streaming requests, failures after HTTP headers are sent arrive as an SSE `error` event with this same error object. Never treat partial text as a completed result or append output from a retry. ## Rate limits and quotas Defaults: Free **15 requests per 60 seconds** and **500 accepted calls per key per UTC calendar month**; Pro placeholders **120/60 seconds** and **25,000/month**. The operator can change plans; response headers are authoritative. Calls to all tools share a key's limits. Accepted cache hits and upstream failures consume quota. Authentication/schema failures and provider-not-configured preflight failures do not. `X-RateLimit-Remaining`, `X-RateLimit-Reset`, `X-Quota-Remaining` and `X-Quota-Reset` show remaining capacity and Unix-second reset times. For HTTP 429 wait `Retry-After` seconds; quota/budget limits need a reset or operator action. Requests may also receive 429 when the shared processing slots are busy. See [retries and long texts](https://metaphrase.msalman.com/docs/reliability.md). ## Code samples These are complete backend programs, not frontend key examples. JavaScript fetch and Node.js are separate options; the latter uses the official SDK. Flutter UI must call your own backend; the Dart sample runs on a backend/CLI. See [mobile integration](https://metaphrase.msalman.com/docs/recipe-mobile.md). ### cURL Save as `request.sh`. Run: `bash request.sh`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```bash #!/usr/bin/env bash set -euo pipefail : "${METAPHRASE_API_KEY:?Set METAPHRASE_API_KEY in your backend environment}" BASE_URL="${METAPHRASE_BASE_URL:-https://metaphrase.msalman.com}" curl --fail-with-body --silent --show-error \ --request POST "${BASE_URL%/}/api/v1/cite" \ --header "Authorization: Bearer $METAPHRASE_API_KEY" \ --header 'Content-Type: application/json' \ --data-binary @- <<'JSON' { "source_type": "manual", "style": "apa", "fields": { "type": "book", "title": "A clear idea", "authors": [ { "given": "Ada", "family": "Lovelace" } ], "year": 2024, "publisher": "Example Press" }, "stream": false } JSON ``` ### JavaScript (fetch) Save as `request.mjs`. Run: `node request.mjs (Node.js 22+; backend only)`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```javascript const key = process.env.METAPHRASE_API_KEY; if (!key) throw new Error("Set METAPHRASE_API_KEY on your backend"); const base = (process.env.METAPHRASE_BASE_URL ?? "https://metaphrase.msalman.com").replace(/\/$/, ""); const payload = { "source_type": "manual", "style": "apa", "fields": { "type": "book", "title": "A clear idea", "authors": [ { "given": "Ada", "family": "Lovelace" } ], "year": 2024, "publisher": "Example Press" }, "stream": false }; const response = await fetch(base + "/api/v1/cite", { method: "POST", headers: { Authorization: "Bearer " + key, "Content-Type": "application/json" }, body: JSON.stringify(payload), signal: AbortSignal.timeout(250000), }); const result = await response.json(); if (!response.ok) { console.error(response.status, result.error.code, result.error.message, result.error.docs_url); process.exitCode = 1; } else { console.log(JSON.stringify(result, null, 2)); } ``` ### Node.js Save as `request-sdk.mjs`. Run: `npm install https://metaphrase.msalman.com/downloads/metaphrase-sdk-1.0.0.tgz && node request-sdk.mjs`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```javascript import { MetaPhrase, MetaPhraseError } from "@metaphrase/sdk"; const client = new MetaPhrase({ api_key: process.env.METAPHRASE_API_KEY, base_url: process.env.METAPHRASE_BASE_URL ?? "https://metaphrase.msalman.com", }); try { const result = await client.cite({ "source_type": "manual", "style": "apa", "fields": { "type": "book", "title": "A clear idea", "authors": [ { "given": "Ada", "family": "Lovelace" } ], "year": 2024, "publisher": "Example Press" }, "stream": false }); console.log(JSON.stringify(result, null, 2)); } catch (error) { if (error instanceof MetaPhraseError) console.error(error.status, error.code, error.message, error.docs_url); else console.error(error); process.exitCode = 1; } ``` ### Python Save as `request.py`. Run: `python3 request.py (Python 3.11+; standard library only)`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```python import json import os import sys from urllib.request import Request, urlopen from urllib.error import HTTPError key = os.environ["METAPHRASE_API_KEY"] base = os.environ.get("METAPHRASE_BASE_URL", "https://metaphrase.msalman.com").rstrip("/") payload = json.loads("{\"source_type\":\"manual\",\"style\":\"apa\",\"fields\":{\"type\":\"book\",\"title\":\"A clear idea\",\"authors\":[{\"given\":\"Ada\",\"family\":\"Lovelace\"}],\"year\":2024,\"publisher\":\"Example Press\"},\"stream\":false}") request = Request( base + "/api/v1/cite", data=json.dumps(payload, ensure_ascii=False).encode("utf-8"), headers={"Authorization": "Bearer " + key, "Content-Type": "application/json"}, method="POST", ) try: with urlopen(request, timeout=250) as response: print(json.dumps(json.load(response), ensure_ascii=False, indent=2)) except HTTPError as error: print(error.code, error.read().decode("utf-8"), file=sys.stderr) sys.exit(1) ``` ### PHP Save as `request.php`. Run: `php request.php (PHP 8.2+ with the cURL extension)`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 250, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key, 'Content-Type: application/json'], CURLOPT_POSTFIELDS => json_encode($payload, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE), ]); $body = curl_exec($curl); if ($body === false) { throw new RuntimeException(curl_error($curl)); } $status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE); curl_close($curl); $result = json_decode($body, true, 512, JSON_THROW_ON_ERROR); echo json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . PHP_EOL; if ($status >= 400) { exit(1); } ``` ### Java Save as `MetaPhraseExample.java`. Run: `javac -encoding UTF-8 MetaPhraseExample.java && java MetaPhraseExample (JDK 17+)`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```java import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Duration; public class MetaPhraseExample { public static void main(String[] args) throws Exception { String key = System.getenv("METAPHRASE_API_KEY"); if (key == null || key.isBlank()) throw new IllegalStateException("Set METAPHRASE_API_KEY"); String base = System.getenv().getOrDefault("METAPHRASE_BASE_URL", "https://metaphrase.msalman.com").replaceAll("/+$", ""); String payload = """ { "source_type": "manual", "style": "apa", "fields": { "type": "book", "title": "A clear idea", "authors": [ { "given": "Ada", "family": "Lovelace" } ], "year": 2024, "publisher": "Example Press" }, "stream": false } """; HttpRequest request = HttpRequest.newBuilder(URI.create(base + "/api/v1/cite")) .timeout(Duration.ofSeconds(250)) .header("Authorization", "Bearer " + key) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(payload)) .build(); HttpResponse response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString()); System.out.println(response.body()); if (response.statusCode() >= 400) throw new java.io.IOException("HTTP " + response.statusCode() + ": " + response.body()); } } ``` ### C# Save as `Program.cs`. Run: `dotnet new console -n MetaPhraseExample; replace its Program.cs, then dotnet run --project MetaPhraseExample (.NET 8+)`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```csharp using System.Net.Http.Headers; using System.Text; string key = Environment.GetEnvironmentVariable("METAPHRASE_API_KEY") ?? throw new InvalidOperationException("Set METAPHRASE_API_KEY"); string baseUrl = (Environment.GetEnvironmentVariable("METAPHRASE_BASE_URL") ?? "https://metaphrase.msalman.com").TrimEnd('/'); string payload = """ { "source_type": "manual", "style": "apa", "fields": { "type": "book", "title": "A clear idea", "authors": [ { "given": "Ada", "family": "Lovelace" } ], "year": 2024, "publisher": "Example Press" }, "stream": false } """; using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(250) }; client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", key); using var content = new StringContent(payload, Encoding.UTF8, "application/json"); using var response = await client.PostAsync(baseUrl + "/api/v1/cite", content); string result = await response.Content.ReadAsStringAsync(); Console.WriteLine(result); if (!response.IsSuccessStatusCode) throw new HttpRequestException($"HTTP {(int)response.StatusCode}: {result}"); ``` ### Go Save as `main.go`. Run: `go run main.go (Go 1.22+; standard library only)`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```go package main import ( "bytes" "fmt" "io" "net/http" "os" "strings" "time" ) func main() { key := os.Getenv("METAPHRASE_API_KEY") if key == "" { panic("Set METAPHRASE_API_KEY") } base := os.Getenv("METAPHRASE_BASE_URL") if base == "" { base = "https://metaphrase.msalman.com" } payload := []byte(`{ "source_type": "manual", "style": "apa", "fields": { "type": "book", "title": "A clear idea", "authors": [ { "given": "Ada", "family": "Lovelace" } ], "year": 2024, "publisher": "Example Press" }, "stream": false }`) request, err := http.NewRequest(http.MethodPost, strings.TrimRight(base, "/") + "/api/v1/cite", bytes.NewReader(payload)) if err != nil { panic(err) } request.Header.Set("Authorization", "Bearer " + key) request.Header.Set("Content-Type", "application/json") client := &http.Client{Timeout: 250 * time.Second} response, err := client.Do(request) if err != nil { panic(err) } defer response.Body.Close() body, err := io.ReadAll(response.Body) if err != nil { panic(err) } fmt.Println(string(body)) if response.StatusCode >= 400 { os.Exit(1) } } ``` ### Flutter / Dart Save as `request.dart`. Run: `dart run request.dart (Dart 3+ backend/CLI; Flutter UI calls your backend proxy)`. Requires `METAPHRASE_API_KEY` in your backend environment. Set optional `METAPHRASE_BASE_URL` to override the origin. ```dart import 'dart:convert'; import 'dart:io'; Future main() async { final key = Platform.environment['METAPHRASE_API_KEY']; if (key == null || key.isEmpty) throw StateError('Set METAPHRASE_API_KEY on your backend'); final base = (Platform.environment['METAPHRASE_BASE_URL'] ?? 'https://metaphrase.msalman.com').replaceFirst(RegExp(r'/+$'), ''); final payload = jsonDecode(r'''{"source_type":"manual","style":"apa","fields":{"type":"book","title":"A clear idea","authors":[{"given":"Ada","family":"Lovelace"}],"year":2024,"publisher":"Example Press"},"stream":false}'''); final client = HttpClient()..connectionTimeout = const Duration(seconds: 30); try { final request = await client.postUrl(Uri.parse('$base/api/v1/cite')); request.headers.set(HttpHeaders.authorizationHeader, 'Bearer $key'); request.headers.contentType = ContentType.json; request.write(jsonEncode(payload)); final response = await request.close().timeout(const Duration(seconds: 250)); final body = await utf8.decoder.bind(response).join(); stdout.writeln(body); if (response.statusCode >= 400) exitCode = 1; } finally { client.close(force: true); } } ``` --- # Streaming responses Use Server-Sent Events for paraphrase rewrites, translation and summarization. Send `stream: true` in the JSON body. `Accept: text/event-stream` is optional; the body flag selects streaming. Grammar, detector and citation requests require `stream: false`. Paraphrase `action: alternatives` always returns JSON, including when `stream: true` is sent. ```bash curl --no-buffer --fail-with-body --silent --show-error \ 'https://metaphrase.msalman.com/api/v1/paraphrase' \ -H "Authorization: Bearer $METAPHRASE_API_KEY" \ -H 'Content-Type: application/json' \ --data '{"text":"Good writing helps people understand an idea.","mode":"standard","stream":true}' ``` A successful streaming response has HTTP 200 and `Content-Type: text/event-stream`. Event order: 1. `meta`: `{"request_id":"","tool":"paraphrase"}`. 2. Zero or more `delta`: `{"text_delta":"a piece of output"}`. Append only `text_delta`; do not append the entire event JSON. 3. Exactly one terminal event: `done` with the **full ordinary JSON response envelope**, or `error` with the same `{"error":{"code","message","docs_url"}}` object used by JSON errors. `done.result.text` is authoritative; replace your accumulated preview with it. A cache hit can send one delta containing the full result, followed by done with zero new usage. Local key-sentence summaries can also stream one complete result. Heartbeats are SSE comment lines beginning with `:` and must be ignored. Validation/auth/quota/preflight failures occur before headers and use ordinary non-200 JSON. Once streaming begins, HTTP status cannot change; an `error` event may arrive after some deltas. If the connection closes without `done`, the result is incomplete. Preserve it as a partial preview, display the error, and do not cache it as a successful answer. Use the SDK's streaming helper to handle UTF-8 boundaries and SSE framing: ```javascript import { MetaPhrase } from '@metaphrase/sdk'; const client = new MetaPhrase({api_key:process.env.METAPHRASE_API_KEY}); let partial=''; try { for await (const event of client.stream('paraphrase',{text:'Good writing makes an idea easier to understand.',mode:'standard'})) { if(event.event==='delta'){partial+=event.data.text_delta;process.stdout.write(event.data.text_delta);} if(event.event==='done')console.log('\nCompleted:',event.data.result.text,event.data.usage); } } catch(error) { console.error('Partial result:',partial); console.error(error.code,error.message,error.docs_url); } ``` Install the SDK using [the SDK guide](https://metaphrase.msalman.com/docs/sdks.md). The Python helper is `client.stream('paraphrase', {'text': '...'})` and yields dictionaries with `event` and `data`. Both SDKs throw a `MetaPhraseError` on an error event or an incomplete stream. Browser `EventSource` cannot send a POST body or Bearer header; stream through your backend and use `fetch` streaming in the UI instead. Abort the fetch/SDK stream when the user selects Stop. Cancellation can still incur upstream tokens; accepted calls still consume quota. Provider fallback happens only before any output is emitted, so two providers' output is never mixed. If you retry, start a separate result and replace the old preview rather than appending to it. --- # Long texts, caching, limits and retries Bound requests, preserve sentence order and retry only errors that can recover. The current input cap is 20000 **Unicode code points**, including an emoji as one character. JSON request bodies are capped at 120,000 bytes by default. Limits can be changed by the operator; OpenAPI and response headers describe the deployed settings. Never count UTF-8 bytes or JavaScript UTF-16 `.length` as equivalent to the text character limit. The server automatically divides accepted generative text into bounded sequential chunks. Default provider chunks are at most 6,000 code points and 5,000 UTF-8 bytes; per-tool token caps apply to each provider call. Paraphrase/translation preserve chunk order. Generative summaries summarize bounded sections separately. This prevents large input from silently exceeding a provider context window. A request can still time out after four minutes; use smaller client-side sections for large documents. For input above the API cap, split at sentence/paragraph boundaries, process sections sequentially and join results in order. This complete Node program handles long individual sentences without splitting surrogate pairs: ```javascript import {readFile} from 'node:fs/promises'; const key=process.env.METAPHRASE_API_KEY; if(!key)throw new Error('Set METAPHRASE_API_KEY'); const limit=20000; const source=await readFile(process.argv[2]??'input.txt','utf8'); const chunks=[];let current=''; for(const {segment} of new Intl.Segmenter(undefined,{granularity:'sentence'}).segment(source)){ const points=[...segment]; for(let index=0;indexlimit){if(current.trim())chunks.push(current);current='';} current+=part; } } if(current.trim())chunks.push(current); const results=[]; for(const text of chunks){ const response=await fetch('https://metaphrase.msalman.com/api/v1/paraphrase',{method:'POST',headers:{Authorization:'Bearer '+key,'Content-Type':'application/json'},body:JSON.stringify({text,mode:'standard'}),signal:AbortSignal.timeout(250000)}); const value=await response.json(); if(!response.ok)throw new Error(value.error.code+': '+value.error.message+' '+value.error.docs_url); results.push(value.result.text); } console.log(results.join('\n\n')); ``` Save as `long-text.mjs`; run `node long-text.mjs input.txt`. Each section consumes its own accepted-call quota. Very large numbers of tiny requests cost more in prompt overhead, so avoid splitting every sentence into a separate API call. ## Cache and usage Identical tool inputs share a private 24-hour result cache. Streaming and JSON forms share the same generative result. Cache hits have `usage.cached=true`, zero new tokens and zero new provider cost; they still consume a key's request quota. Cache admission may be skipped under memory pressure, so repeated requests are not guaranteed to hit cache. Concurrent identical work may return 429 with `Retry-After: 2` rather than start a second paid provider call. Usage is exact when the provider reports tokens; otherwise `usage_estimated=true`. Accepted failed or cancelled upstream attempts may be billed. A provider fallback can therefore include measured/estimated units from more than one attempt. `estimated_cost_usd` uses operator-configured per-million token prices and is an estimate, not a payment charge or invoice. Free grammar, citation and extractive summaries have zero LLM tokens. ## Retry policy - `invalid_request`, `invalid_json`, `invalid_source`, `text_too_long` and `text_too_short`: correct input before retrying. - `unauthorized`: replace the key; do not loop on the same revoked key. - `provider_not_configured`: operator configuration is required; changing your key does not fix it. - `rate_limited`: wait the response's `Retry-After` seconds and add jitter. It can mean per-key rate, duplicate work, or occupied shared slots. - `quota_exceeded` / `budget_exceeded`: wait for reset or ask the operator. Do not repeatedly retry a monthly/daily cap. - `upstream_unavailable`, `service_unavailable`: use bounded exponential backoff; a later retry may succeed. - `upstream_timeout`, `upstream_error`, `output_truncated`: preserve any partial preview and retry a shorter section as a separate request. An earlier attempt may have incurred tokens. The official SDK does not automatically retry paid work. A simple bounded helper for ordinary JSON calls: ```javascript import {setTimeout as delay} from 'node:timers/promises'; const key=process.env.METAPHRASE_API_KEY; if(!key)throw new Error('Set METAPHRASE_API_KEY'); async function callWithRetry(payload){ for(let attempt=0;attempt<3;attempt++){ const response=await fetch('https://metaphrase.msalman.com/api/v1/paraphrase',{method:'POST',headers:{Authorization:'Bearer '+key,'Content-Type':'application/json'},body:JSON.stringify(payload),signal:AbortSignal.timeout(250000)}); const value=await response.json(); if(response.ok)return value; const code=value.error?.code; if(!['rate_limited','upstream_unavailable','service_unavailable'].includes(code)||attempt===2)throw new Error(JSON.stringify(value.error)); const retry=Number(response.headers.get('retry-after')); const wait=Number.isFinite(retry)&&retry>0?retry*1000:500*2**attempt; if(wait>60000)throw new Error(JSON.stringify(value.error)); await delay(wait+Math.random()*250); } } console.log(await callWithRetry({text:'A clear idea deserves careful expression.'})); ``` Save as `retry.mjs`; run `node retry.mjs`. No outgoing webhooks are implemented. Use ordinary HTTP responses or SSE; Zapier/Make can trigger an incoming workflow and then call this API. --- # Official JavaScript and Python SDKs Use small dependency-free clients with types generated from the deployed OpenAPI contract. ## JavaScript / TypeScript Requires Node.js 22+ or a compatible server-side fetch runtime. Install the official downloadable npm archive: ```bash npm install https://metaphrase.msalman.com/downloads/metaphrase-sdk-1.0.0.tgz ``` Save as `example.mjs`, set `METAPHRASE_API_KEY`, then run `node example.mjs`: ```javascript import {MetaPhrase,MetaPhraseError} from '@metaphrase/sdk'; const client=new MetaPhrase({api_key:process.env.METAPHRASE_API_KEY}); try{ const response=await client.grammar({text:'The best ideas deserves to be shared.',language:'en-US'}); console.log(response.result.text,response.usage); }catch(error){ if(error instanceof MetaPhraseError)console.error(error.status,error.code,error.message,error.docs_url); else throw error; } ``` All methods are named `paraphrase`, `grammar`, `detect`, `translate`, `summarize`, and `cite` and accept one typed request object. They return the full typed response envelope, not just text. Constructor options use snake_case: `api_key`, optional `base_url` (origin, without `/api/v1`), and `timeout_ms` (default 250,000). A custom `fetch` can be provided for server runtimes/tests. Pass `{signal: AbortSignal}` as the second method argument to cancel. Use `client.stream('paraphrase', request)` for streaming; do not pass `stream:true` to an ordinary JSON method. See [streaming](https://metaphrase.msalman.com/docs/streaming.md). Secret-key SDKs belong on the backend; the JavaScript client rejects browser construction. ## Python Requires Python 3.11+. Create a virtual environment and install the official wheel: ```bash python3 -m venv .venv . .venv/bin/activate python -m pip install https://metaphrase.msalman.com/downloads/metaphrase_sdk-1.0.0-py3-none-any.whl ``` Save as `example.py`, set `METAPHRASE_API_KEY`, then run `python example.py`: ```python import os from metaphrase_sdk import MetaPhrase, MetaPhraseError client = MetaPhrase(api_key=os.environ['METAPHRASE_API_KEY']) try: result = client.grammar({'text': 'The best ideas deserves to be shared.', 'language': 'en-US'}) print(result['result']['text']) print(result['usage']) except MetaPhraseError as error: print(error.status, error.code, str(error), error.docs_url) ``` The import package is `metaphrase_sdk` (with `phrase`); use that exact name. Constructor options: `api_key`, `base_url` (origin), and `timeout` in seconds (default 250). Methods accept a typed request dictionary and return typed response dictionaries; types are exported from `metaphrase_sdk.types`. The Python client uses only the standard library and does not automatically retry. For streaming: ```python import os from metaphrase_sdk import MetaPhrase client = MetaPhrase(api_key=os.environ['METAPHRASE_API_KEY']) for event in client.stream('paraphrase', {'text': 'Good writing makes an idea easier to understand.'}): if event['event'] == 'delta': print(event['data']['text_delta'], end='', flush=True) elif event['event'] == 'done': print('\n', event['data']['usage']) ``` ## Versioning and publication SDK 1.0.0 targets REST `/api/v1`. The checked-in generator reads the same OpenAPI schemas used by the API docs. Every build regenerates client types, methods, downloads, Postman and Insomnia files. Contract checks detect differences between generated artifacts and the source schemas. The archives above work with npm and pip without a registry account. Publication under npm/PyPI package names requires the operator's registry ownership and credentials; do not assume a similarly named public package is this official SDK. Install from this domain until registry publication is verified. The package source includes publication instructions and a specification checksum. --- # Embeddable paraphraser widget Add a MetaPhrase writing box with one script tag and no frontend secret key. 1. Sign in to [the dashboard](https://metaphrase.msalman.com/dashboard) and create a secret API key for the widget's quota. 2. Under **Embeddable widgets**, select **Create widget**. 3. Choose the linked key and enter exact allowed website origins, for example `https://example.com` and `https://www.example.com`. Do not include paths or wildcards. 4. Copy the generated script to the part of the page where the box should appear. ```html ``` Replace only the public widget ID with the value generated by your dashboard. **Never put the secret API key in this tag.** Each tag inserts a responsive iframe after itself. The iframe is titled for accessibility, uses the website's light/dark preference, and supports seven paraphrasing modes, result copy and clear errors. The iframe is served from MetaPhrase. An explicit CSP `frame-ancestors` policy restricts which sites may embed it. Public IDs are not authentication secrets: the origin list controls browser embedding, while quotas, per-IP limits and heavy-use verification control abuse. Requests share the linked secret key's API quota and the free-web IP limits. Revoking a widget disables that public box; revoking its linked API key disables every linked widget. Heavy use may display a Turnstile challenge. Without operator-configured Turnstile credentials, the free daily threshold stops further requests until reset. Missing LLM credentials produce a clear provider-unavailable error rather than a simulated rewrite. If the box does not appear, confirm that the ID is active, its linked key is active, your origin including scheme/port is allowed, and your site's CSP permits scripts and frames from `https://metaphrase.msalman.com`. Use an actual HTTP(S) site for development; `file://` pages have an unsupported null origin. No backend secret is needed in the embedding site. --- # Errors and troubleshooting Every API failure has a stable code, a readable message and a link to its exact fix. ```json { "error": { "code": "unauthorized", "message": "A valid API key is required.", "docs_url": "https://metaphrase.msalman.com/docs/errors#unauthorized" } } ``` Use HTTP status and `error.code` in program logic, not English wording. Keep `X-Request-Id` for support. For SSE, HTTP 200 can contain an `error` event after output starts; only `done` marks success. See [streaming](https://metaphrase.msalman.com/docs/streaming.md). ## invalid_file **HTTP 422.** This document could not be read. **Fix:** Upload an unencrypted .txt (UTF-8), .docx, or text-based .pdf file, at most 5 MB and within max_input_chars. Scanned PDFs need OCR first. ## invalid_request **HTTP 400.** Check the request fields and their allowed values. **Fix:** Send the documented JSON fields with the correct types. Unknown fields are rejected. ## invalid_json **HTTP 400.** The request body is not valid JSON. **Fix:** Serialize the request with JSON.stringify or your language's JSON encoder. ## unsupported_media_type **HTTP 415.** Send application/json content. **Fix:** Set Content-Type: application/json. File uploads use their separate documented route. ## body_too_large **HTTP 413.** The request body exceeds the allowed size. **Fix:** Reduce the payload and split long documents at sentence boundaries. ## text_too_long **HTTP 413.** The text exceeds the configured character limit. **Fix:** Use the max_input_chars value in the API specification and split longer documents. ## unauthorized **HTTP 401.** A valid API key is required. **Fix:** Send Authorization: Bearer YOUR_KEY. Create a new key if the old one was revoked. ## forbidden **HTTP 403.** This action is not available to this account. **Fix:** Check account status and the key's access permissions. ## rate_limited **HTTP 429.** Too many requests. Please wait and try again. **Fix:** Wait for Retry-After seconds and use exponential backoff with jitter. ## quota_exceeded **HTTP 429.** The monthly usage quota has been reached. **Fix:** Wait for the next UTC calendar month or ask an administrator to change the plan. ## budget_exceeded **HTTP 429.** The writing service has reached its configured spending limit. **Fix:** The operator must review usage and adjust the configured budget before more paid requests can run. ## verification_required **HTTP 403.** Please complete the verification challenge. **Fix:** Complete Turnstile in the web interface, then submit the new verification token. ## provider_not_configured **HTTP 503.** The writing provider is not configured yet. **Fix:** The operator must configure LLM_PRIMARY_API_KEY or a fallback provider in .env and recreate the web container. ## upstream_unavailable **HTTP 503.** The writing provider is temporarily unavailable. **Fix:** Retry with backoff. The operator can inspect provider availability without logging submitted text. ## upstream_timeout **HTTP 504.** The writing provider took too long to respond. **Fix:** Retry a shorter input or wait before retrying. Do not append a retry to partial streamed output. ## upstream_error **HTTP 502.** The writing provider returned an invalid response. **Fix:** Retry once with a shorter input. If it continues, the operator should check the configured model's compatibility. ## output_truncated **HTTP 502.** The response reached its output limit before finishing. **Fix:** Use a shorter input or ask the operator to adjust the tool's output cap and chunk size. ## service_unavailable **HTTP 503.** A required service is temporarily unavailable. **Fix:** Wait and retry. The operator should check Redis, PostgreSQL, or the tool's health. ## invalid_source **HTTP 422.** The source could not be used. **Fix:** Check the DOI, ISBN, or public HTTP URL, or use manual citation fields. ## source_unavailable **HTTP 422.** Source metadata could not be retrieved. **Fix:** Try a DOI or ISBN, or enter the source details manually. ## text_too_short **HTTP 422.** More text is needed for a meaningful estimate. **Fix:** Submit at least the documented minimum word count. Very short texts cannot be assessed meaningfully. ## not_found **HTTP 404.** This resource does not exist. **Fix:** Check the API version, endpoint name, and resource identifier. ## method_not_allowed **HTTP 405.** This HTTP method is not supported. **Fix:** Use POST for processing endpoints and the method documented for dashboard routes. ## conflict **HTTP 409.** This action conflicts with an existing resource. **Fix:** Use a different identifier or retry after the existing operation has completed. ## internal_error **HTTP 500.** The request could not be completed. **Fix:** Retry once. If it persists, provide the X-Request-Id header to the operator. ## Troubleshooting FAQ **I received HTML instead of JSON.** Check the exact HTTPS origin and path, your reverse proxy/CDN rules and the Content-Type. A CDN challenge or gateway failure is outside the API's JSON handler. SDKs surface it as an upstream error. **My browser integration works but exposes a key.** Move the upstream request into a backend route; use [the working proxy](https://metaphrase.msalman.com/docs/recipe-html.md). CORS does not make a secret safe in frontend code. **My provider is not configured.** This is an operator setting. Grammar, citations and extractive summaries still work. Generative tools require a provider configured on MetaPhrase, not an OpenAI key from every API consumer. **My grammar offsets are wrong with emoji.** Offsets/lengths use UTF-16 code units. JavaScript slice works directly; Python clients need UTF-16 conversion, as shown on the grammar page. **My request succeeded but quota decreased for a cache hit.** Accepted calls count against monthly quota; cache hits avoid model tokens, not request accounting. All tools share each key's cap. **The SDK raised an error with status=null.** This is a client network/timeout failure before an HTTP status was received. Check connectivity and the configured base origin. Retrying paid work may incur another attempt, so use bounded retries. **The widget is blank or denied.** Check the public widget ID, active linked key, exact scheme/host/port origin and your site's script/frame CSP. See [widget setup](https://metaphrase.msalman.com/docs/widget.md). --- # Plain HTML website with a backend proxy Run a complete HTML paraphraser while keeping your MetaPhrase key on the server. Requires Node.js 22+. Create an empty folder with the two files below. No npm dependencies are needed. ```bash export METAPHRASE_API_KEY='your_secret_key' node server.mjs ``` Open `http://127.0.0.1:4000`. The Node server binds to loopback. To publish it, put it behind your HTTPS reverse proxy and set `PUBLIC_ORIGIN` to your website origin. Keep your site's authentication or stronger abuse controls for a public production form. This example already applies a 15/minute IP limit, caps active IP records, bounds JSON/text length and validates browser Origin. Save as `server.mjs`: ```javascript import http from 'node:http'; import { readFile } from 'node:fs/promises'; const key = process.env.METAPHRASE_API_KEY; if (!key) throw new Error('Set METAPHRASE_API_KEY on the backend'); const apiOrigin = (process.env.METAPHRASE_BASE_URL ?? 'https://metaphrase.msalman.com').replace(/\/$/, ''); const port = Number(process.env.PORT ?? 4000); const publicOrigin = process.env.PUBLIC_ORIGIN ?? `http://127.0.0.1:${port}`; const windows = new Map(); function reply(response, status, value) { response.writeHead(status, {'Content-Type':'application/json', 'Cache-Control':'no-store'}); response.end(JSON.stringify(value)); } const server = http.createServer(async (request, response) => { try { if (request.method === 'GET' && request.url === '/') { response.writeHead(200, {'Content-Type':'text/html; charset=utf-8'}); response.end(await readFile(new URL('./index.html', import.meta.url))); return; } if (request.method !== 'POST' || request.url !== '/api/metaphrase') { reply(response, 404, {error:{message:'Route not found'}}); return; } if (request.headers.origin && request.headers.origin !== publicOrigin) { reply(response, 403, {error:{message:'Origin not allowed'}}); return; } const now = Date.now(); for (const [ip, value] of windows) if (value.expires <= now) windows.delete(ip); const ip = request.socket.remoteAddress ?? 'unknown'; let window = windows.get(ip); if (!window) { if (windows.size >= 10000) { reply(response, 503, {error:{message:'Please retry shortly'}}); return; } window = {count:0, expires:now+60000}; windows.set(ip, window); } if (++window.count > 15) { response.setHeader('Retry-After','60'); reply(response,429,{error:{message:'Please wait a minute'}}); return; } if (!request.headers['content-type']?.startsWith('application/json')) { reply(response,415,{error:{message:'Send application/json'}}); return; } let size=0; const chunks=[]; for await (const chunk of request) { size+=chunk.length; if (size>120000) {reply(response,413,{error:{message:'Text is too long'}});return;} chunks.push(chunk); } let payload; try {payload=JSON.parse(Buffer.concat(chunks).toString('utf8'));} catch {reply(response,400,{error:{message:'Invalid JSON'}});return;} if (typeof payload.text !== 'string' || !payload.text.trim() || [...payload.text].length>20000) { reply(response,400,{error:{message:'Enter text within the character limit'}});return; } const upstream=await fetch(apiOrigin+'/api/v1/paraphrase', { method:'POST', headers:{Authorization:'Bearer '+key,'Content-Type':'application/json'}, body:JSON.stringify({text:payload.text,mode:'standard',synonym_strength:50,stream:false}), signal:AbortSignal.timeout(250000), }); const data=await upstream.json(); if (upstream.headers.has('retry-after')) response.setHeader('Retry-After',upstream.headers.get('retry-after')); reply(response,upstream.status,data); } catch { if (!response.headersSent) reply(response,502,{error:{message:'The writing service could not be reached. Please retry.'}}); else response.end(); } }); server.requestTimeout=30000; server.listen(port,'127.0.0.1',()=>console.log(`Open ${publicOrigin}`)); ``` Save as `index.html`: ```html My paraphraser

A fresh way to say it

Result

``` The browser never receives `METAPHRASE_API_KEY`. It reads the result from `data.result.text`; errors come from `data.error.message`. `provider_not_configured` means the MetaPhrase operator must configure a provider, not that this proxy needs a different payload. --- # React and Next.js integration Keep the MetaPhrase key in an App Router backend route and call that route from React. Create a TypeScript App Router project, then add the following files. Requires Node.js 22+. ```bash npx create-next-app@16.4.2 my-writing-app --ts --app --no-src-dir --use-npm --yes cd my-writing-app ``` Set `METAPHRASE_API_KEY` in `.env.local` on the server, without a `NEXT_PUBLIC_` prefix. Do not commit it. Optional `METAPHRASE_BASE_URL` overrides the API origin. Save as `app/api/metaphrase/route.ts`: ```typescript export const runtime='nodejs'; let windowStart=0, accepted=0; export async function POST(request:Request){ const now=Date.now();if(now-windowStart>=60000){windowStart=now;accepted=0;} if(++accepted>30)return Response.json({error:{message:'Please retry in a minute.'}},{status:429,headers:{'Retry-After':'60'}}); const origin=request.headers.get('origin'); if(origin && origin!==new URL(request.url).origin)return Response.json({error:{message:'Origin not allowed'}},{status:403}); const key=process.env.METAPHRASE_API_KEY; if(!key)return Response.json({error:{message:'The backend key is not configured.'}},{status:503}); if(Number(request.headers.get('content-length'))>120000)return Response.json({error:{message:'Request too large'}},{status:413}); const reader=request.body?.getReader();if(!reader)return Response.json({error:{message:'Missing JSON body'}},{status:400}); const chunks:Uint8Array[]=[];let size=0; try{while(true){const {done,value}=await reader.read();if(done)break;size+=value.length;if(size>120000){await reader.cancel();return Response.json({error:{message:'Request too large'}},{status:413});}chunks.push(value);}}finally{reader.releaseLock();} let payload:unknown; try{payload=JSON.parse(Buffer.concat(chunks).toString('utf8'));}catch{return Response.json({error:{message:'Invalid JSON'}},{status:400});} const text=payload && typeof payload==='object' && 'text' in payload?payload.text:undefined; if(typeof text!=='string'||!text.trim()||[...text].length>20000)return Response.json({error:{message:'Enter text within the character limit.'}},{status:400}); try{ const response=await fetch((process.env.METAPHRASE_BASE_URL??'https://metaphrase.msalman.com').replace(/\/$/,'')+'/api/v1/paraphrase',{method:'POST',headers:{Authorization:'Bearer '+key,'Content-Type':'application/json'},body:JSON.stringify({text,mode:'standard',stream:false}),signal:AbortSignal.timeout(250000),cache:'no-store'}); const headers:Record={'Cache-Control':'no-store'};if(response.headers.has('retry-after'))headers['Retry-After']=response.headers.get('retry-after')!; return Response.json(await response.json(),{status:response.status,headers}); }catch{return Response.json({error:{message:'The writing service is temporarily unavailable.'}},{status:502});} } ``` Save as `app/page.tsx`: ```tsx 'use client'; import {useState,type FormEvent} from 'react'; export default function Page(){ const [text,setText]=useState(''),[output,setOutput]=useState(''),[error,setError]=useState(''),[busy,setBusy]=useState(false); async function submit(event:FormEvent){event.preventDefault();setBusy(true);setError(''); try{const response=await fetch('/api/metaphrase',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({text})});const value=await response.json();if(!response.ok)throw new Error(value.error?.message??'Please retry.');setOutput(value.result.text);} catch(error){setError((error as Error).message);}finally{setBusy(false);}} return

My paraphraser


    
    
    'Refresh the page and retry.'],403); }
    $ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
    $limit_key = 'mp_ip_' . hash_hmac('sha256',$ip,wp_salt('nonce'));
    $count = (int) get_transient($limit_key);
    if ($count >= 15) { header('Retry-After: 60'); wp_send_json_error(['message'=>'Please wait a minute.'],429); }
    set_transient($limit_key,$count+1,60);
    if (!defined('METAPHRASE_API_KEY') || !METAPHRASE_API_KEY) { wp_send_json_error(['message'=>'The server API key is not configured.'],503); }
    $text = isset($_POST['text']) && is_string($_POST['text']) ? sanitize_textarea_field(wp_unslash($_POST['text'])) : '';
    if (!$text || strlen($text) > 80000 || preg_match_all('/./us',$text) > 20000) { wp_send_json_error(['message'=>'Enter text within the character limit.'],400); }
    $response = wp_remote_post('https://metaphrase.msalman.com/api/v1/paraphrase',[
      'timeout'=>250,
      'headers'=>['Authorization'=>'Bearer '.METAPHRASE_API_KEY,'Content-Type'=>'application/json'],
      'body'=>wp_json_encode(['text'=>$text,'mode'=>'standard','stream'=>false]),
    ]);
    if (is_wp_error($response)) { wp_send_json_error(['message'=>'The writing service could not be reached.'],502); }
    $status = wp_remote_retrieve_response_code($response);
    $value = json_decode(wp_remote_retrieve_body($response),true);
    if ($status >= 400) { wp_send_json_error(['message'=>$value['error']['message']??'Please retry.','docs_url'=>$value['error']['docs_url']??''], $status); }
    wp_send_json_success(['text'=>$value['result']['text']]);
}
add_action('wp_ajax_metaphrase_write','metaphrase_write_handler');
add_action('wp_ajax_nopriv_metaphrase_write','metaphrase_write_handler');
```

The nonce protects the browser action; the IP throttle is a conservative demo guard. WordPress transients are not an atomic distributed limiter. For high traffic use your site's authenticated access or an atomic shared limiter, and configure WordPress's trusted reverse proxy handling so client IPs cannot be spoofed. If the site uses a strict CSP, move the inline handler into an enqueued script with your site's nonce policy.

For a simpler public box with no WordPress backend integration, use [the widget script](https://metaphrase.msalman.com/docs/widget.md).


---



# Laravel integration

Use Laravel's HTTP client from a server controller and keep the key in configuration.

In a fresh Laravel application set `METAPHRASE_API_KEY` in private `.env`. Add this entry to the array returned by `config/services.php`, preserving your other service settings:

```php
'metaphrase' => [
    'key' => env('METAPHRASE_API_KEY'),
    'base_url' => env('METAPHRASE_BASE_URL', 'https://metaphrase.msalman.com'),
],
```

Save as `app/Http/Controllers/MetaPhraseController.php`:

```php
validate(['text'=>['required','string','max:20000']]);
        $key = config('services.metaphrase.key');
        if (!$key) return response()->json(['error'=>['message'=>'The backend key is not configured.']],503);
        try {
            $result = Http::withToken($key)->acceptJson()->timeout(250)
                ->post(rtrim(config('services.metaphrase.base_url'),'/').'/api/v1/paraphrase',[
                    'text'=>$data['text'],'mode'=>'standard','stream'=>false,
                ]);
            return response()->json($result->json(),$result->status())
                ->header('Cache-Control','no-store');
        } catch (\Illuminate\Http\Client\ConnectionException $error) {
            return response()->json(['error'=>['message'=>'The writing service could not be reached.']],502);
        }
    }
}
```

For a fresh example, `routes/web.php` can contain:

```php
middleware('throttle:15,1');
```

Save as `resources/views/writer.blade.php`:

```html
My paraphraser

My paraphraser

``` Run `php artisan config:clear` after changing local configuration, then `php artisan serve`. Laravel's web CSRF middleware and throttle remain enabled. For production configure your application's authentication, trusted proxies and shared throttle store as usual. The browser receives no API key. The Blade `{{ csrf_token() }}` expression is Laravel syntax and must remain unchanged. --- # Django and FastAPI backends Call MetaPhrase from a Python server and expose only your own proxy route to the browser. Set `METAPHRASE_API_KEY` in the backend environment. Both examples use Python's standard-library HTTP client for the MetaPhrase call, preserve error JSON, and never render the key. Use the [plain HTML page](https://metaphrase.msalman.com/docs/recipe-html.md) as `index.html`; it already posts to `/api/metaphrase` without a key. ## FastAPI Create a virtual environment, install the framework, and save the complete server below as `app.py` alongside `index.html`. ```bash python3 -m venv .venv . .venv/bin/activate python -m pip install fastapi uvicorn uvicorn app:app --host 127.0.0.1 --port 4000 ``` ```python import asyncio import json import os import threading import time from pathlib import Path from urllib.request import Request, urlopen from urllib.error import HTTPError, URLError from fastapi import FastAPI, Request as WebRequest from fastapi.responses import FileResponse, JSONResponse app = FastAPI() api_key = os.environ['METAPHRASE_API_KEY'] api_origin = os.environ.get('METAPHRASE_BASE_URL', 'https://metaphrase.msalman.com').rstrip('/') public_origin = os.environ.get('PUBLIC_ORIGIN', 'http://127.0.0.1:4000') lock = threading.Lock() window = {'start': 0.0, 'count': 0} def upstream(text): request = Request(api_origin + '/api/v1/paraphrase', data=json.dumps({'text': text, 'mode': 'standard', 'stream': False}).encode(), headers={'Authorization': 'Bearer ' + api_key, 'Content-Type': 'application/json'}, method='POST') try: with urlopen(request, timeout=250) as response: return response.status, json.load(response) except HTTPError as error: return error.code, json.loads(error.read()) except (URLError, TimeoutError): return 502, {'error': {'message': 'The writing service could not be reached.'}} @app.get('/') def index(): return FileResponse(Path(__file__).with_name('index.html')) @app.post('/api/metaphrase') async def paraphrase(request: WebRequest): if request.headers.get('origin') not in (None, public_origin): return JSONResponse({'error': {'message': 'Origin not allowed'}}, status_code=403) with lock: now = time.monotonic() if now - window['start'] >= 60: window.update(start=now, count=0) window['count'] += 1 if window['count'] > 30: return JSONResponse({'error': {'message': 'Please wait a minute.'}}, status_code=429, headers={'Retry-After':'60'}) raw = bytearray() async for chunk in request.stream(): raw.extend(chunk) if len(raw) > 120000: return JSONResponse({'error': {'message': 'Request too large'}}, status_code=413) try: value = json.loads(raw) text = value.get('text') if isinstance(value, dict) else None if not isinstance(text, str) or not text.strip() or len(text) > 20000: raise ValueError() except (ValueError, UnicodeError): return JSONResponse({'error': {'message': 'Enter valid text within the character limit.'}}, status_code=400) status, result = await asyncio.to_thread(upstream, text) return JSONResponse(result, status_code=status, headers={'Cache-Control':'no-store'}) ``` Open `http://127.0.0.1:4000`. The demo guard is a shared 30/minute process limit. Use your production application's authentication and a shared limiter when scaling to multiple workers. ## Django For a fresh project: ```bash python -m pip install Django mkdir django-writer cd django-writer django-admin startproject writer . ``` Copy `index.html` from the HTML recipe beside `manage.py`. In that HTML page's fetch headers add `'X-CSRFToken': document.cookie.split('; ').find(item=>item.startsWith('csrftoken='))?.split('=')[1] ?? ''`. Django issues this cookie on the initial GET below. Do not disable CSRF middleware. Save `writer/views.py`: ```python import json import os from pathlib import Path from urllib.request import Request, urlopen from urllib.error import HTTPError, URLError from django.core.cache import cache from django.http import HttpResponse, JsonResponse from django.views.decorators.csrf import ensure_csrf_cookie from django.views.decorators.http import require_POST @ensure_csrf_cookie def index(request): return HttpResponse((Path(__file__).resolve().parent.parent / 'index.html').read_text(), content_type='text/html') @require_POST def paraphrase(request): cache.add('metaphrase_demo_window', 0, 60) if cache.incr('metaphrase_demo_window') > 30: response = JsonResponse({'error': {'message': 'Please wait a minute.'}}, status=429) response['Retry-After'] = '60' return response if int(request.META.get('CONTENT_LENGTH') or 0) > 120000 or len(request.body) > 120000: return JsonResponse({'error': {'message': 'Request too large'}}, status=413) try: value = json.loads(request.body) text = value.get('text') if isinstance(value, dict) else None if not isinstance(text, str) or not text.strip() or len(text) > 20000: raise ValueError() except (ValueError, UnicodeError): return JsonResponse({'error': {'message': 'Enter valid text within the character limit.'}}, status=400) key = os.environ.get('METAPHRASE_API_KEY') if not key: return JsonResponse({'error': {'message': 'Backend key is not configured.'}}, status=503) base = os.environ.get('METAPHRASE_BASE_URL', 'https://metaphrase.msalman.com').rstrip('/') upstream = Request(base + '/api/v1/paraphrase', data=json.dumps({'text':text,'mode':'standard','stream':False}).encode(), headers={'Authorization':'Bearer '+key,'Content-Type':'application/json'}, method='POST') try: with urlopen(upstream, timeout=250) as response: result, status = json.load(response), response.status except HTTPError as error: result, status = json.loads(error.read()), error.code except (URLError, TimeoutError): result, status = {'error': {'message': 'The writing service could not be reached.'}}, 502 response = JsonResponse(result, status=status) response['Cache-Control'] = 'no-store' return response ``` Save `writer/urls.py`: ```python from django.urls import path from . import views urlpatterns = [path('', views.index), path('api/metaphrase', views.paraphrase)] ``` Run `python manage.py migrate`, then `python manage.py runserver 127.0.0.1:4000`. Use your application's authentication and shared cache/rate controls for public production access. Both examples keep the MetaPhrase secret strictly in the Python process. --- # Android, iOS and Flutter via a backend Mobile apps call your own backend proxy, never the MetaPhrase secret-key endpoint directly. First deploy [the HTML/Node proxy](https://metaphrase.msalman.com/docs/recipe-html.md), [Next.js proxy](https://metaphrase.msalman.com/docs/recipe-nextjs.md), or [Python proxy](https://metaphrase.msalman.com/docs/recipe-python.md) at your own HTTPS origin. Its `POST /api/metaphrase` accepts `{"text":"..."}` and returns the full MetaPhrase response. Replace the example `https://your-app.example/api/metaphrase` with that actual URL. The backend alone stores `METAPHRASE_API_KEY`. Apply your app's normal login/session protection to this proxy. ## iOS (SwiftUI) Create a SwiftUI iOS application in Xcode and replace its app source with this complete file. Set `proxyURL` to your backend URL. No MetaPhrase key is included in the app. ```swift import SwiftUI struct WritingResponse: Decodable { struct Result: Decodable { let text: String } struct Failure: Decodable { let message: String } let result: Result? let error: Failure? } @main struct WriterApp: App { var body: some Scene { WindowGroup { WriterView() } } } struct WriterView: View { @State private var text = "" @State private var output = "" @State private var error = "" @State private var busy = false private let proxyURL = URL(string: "https://your-app.example/api/metaphrase")! var body: some View { NavigationStack { VStack(alignment: .leading, spacing: 16) { Text("Your text").font(.headline) TextEditor(text: $text).frame(minHeight: 150).border(.secondary) Button(busy ? "Writing…" : "Paraphrase") { Task { await paraphrase() } } .disabled(busy || text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) if !error.isEmpty { Text(error).foregroundStyle(.red).accessibilityLabel("Error: \(error)") } ScrollView { Text(output).frame(maxWidth: .infinity, alignment: .leading).textSelection(.enabled) } }.padding().navigationTitle("My paraphraser") } } @MainActor private func paraphrase() async { busy = true; error = "" defer { busy = false } do { var request = URLRequest(url: proxyURL) request.httpMethod = "POST" request.timeoutInterval = 250 request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.httpBody = try JSONEncoder().encode(["text": text]) let (data, response) = try await URLSession.shared.data(for: request) let value = try JSONDecoder().decode(WritingResponse.self, from: data) guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode), let result = value.result else { error = value.error?.message ?? "Please try again." return } output = result.text } catch { self.error = error.localizedDescription } } } ``` ## Android / iOS (Flutter) Run `flutter create my_writer`, then `cd my_writer` and `flutter pub add http`. Replace `lib/main.dart` with this complete app. Launch with `flutter run --dart-define=BACKEND_URL=https://your-app.example/api/metaphrase`. ```dart import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; void main() => runApp(const MaterialApp(home: WriterPage())); class WriterPage extends StatefulWidget { const WriterPage({super.key}); @override State createState() => _WriterPageState(); } class _WriterPageState extends State { final input = TextEditingController(); String output = '', error = ''; bool busy = false; static const backend = String.fromEnvironment('BACKEND_URL'); @override void dispose() { input.dispose(); super.dispose(); } Future paraphrase() async { setState(() { busy = true; error = ''; }); try { if (backend.isEmpty) throw StateError('Set BACKEND_URL to your backend proxy'); final response = await http.post(Uri.parse(backend), headers:{'Content-Type':'application/json'}, body:jsonEncode({'text':input.text})).timeout(const Duration(seconds:250)); final value = jsonDecode(response.body) as Map; if (response.statusCode >= 400) throw Exception(value['error']?['message'] ?? 'Please retry.'); if (mounted) setState(() { output = value['result']['text'] as String; }); } catch (failure) { if (mounted) setState(() { error = failure.toString(); }); } finally { if (mounted) setState(() { busy = false; }); } } @override Widget build(BuildContext context) => Scaffold( appBar:AppBar(title:const Text('My paraphraser')), body:Padding(padding:const EdgeInsets.all(20), child:ListView(children:[ TextField(controller:input,maxLines:7,decoration:const InputDecoration(labelText:'Your text',border:OutlineInputBorder())), const SizedBox(height:16), FilledButton(onPressed:busy ? null : paraphrase, child:Text(busy ? 'Writing…' : 'Paraphrase')), if(error.isNotEmpty) Semantics(liveRegion:true,child:Text(error,style:TextStyle(color:Theme.of(context).colorScheme.error))), const SizedBox(height:20),SelectableText(output), ])), ); } ``` Keep Android's INTERNET permission enabled in the release manifest (`` inside ``, outside ``). Use HTTPS so iOS App Transport Security and Android cleartext restrictions need no exceptions. `BACKEND_URL` is public app configuration, not a secret key. Never replace it with a MetaPhrase API key. --- # Lovable, Bolt, Zapier and Make Use server actions or a backend webhook step so your secret key never enters a public page. ## Lovable, Bolt and AI app builders Add `METAPHRASE_API_KEY` to the platform's **server secrets** or backend/edge-function environment. Do not use a frontend/Vite/public variable. Ask the assistant: > Integrate the MetaPhrase paraphrase API into my app. Read https://metaphrase.msalman.com/llms-full.txt first. The secret is in server env var METAPHRASE_API_KEY. Create a server/edge proxy that accepts text, calls POST https://metaphrase.msalman.com/api/v1/paraphrase with Authorization: Bearer from that environment variable and application/json, and returns the full JSON envelope. The frontend calls only my proxy and reads result.text. Validate text length, use my app's login and rate controls, handle error.code/message/docs_url and Retry-After, and never expose or log the secret. Do not invent request fields. The expected upstream body is: ```json {"text":"Good writing helps people understand an idea.","mode":"standard","synonym_strength":50,"stream":false} ``` The server reads the API key from its secret store. The frontend displays `response.result.text`; it must not assume the response is just a string. The [Next.js](https://metaphrase.msalman.com/docs/recipe-nextjs.md) and [plain HTML proxy](https://metaphrase.msalman.com/docs/recipe-html.md) show working implementations you can adapt to the platform's backend. ## Zapier 1. Choose your trigger, for example a form submission or **Webhooks by Zapier → Catch Hook**. Treat the incoming hook URL as a private workflow capability. 2. Add **Webhooks by Zapier → Custom Request** with method POST and URL `https://metaphrase.msalman.com/api/v1/paraphrase`. 3. Set `Content-Type` to `application/json` and `Authorization` to `Bearer ` followed by your key from a private connection/secret field. Keep it out of public form fields. 4. Send the JSON body above, mapping the trigger's text into `text`. Keep `stream:false`; ordinary workflow HTTP steps expect one JSON response. 5. Map the output `result.text` into the next action. Store `request_id` if you need support or traceability. 6. On 429 honor `Retry-After`; on `quota_exceeded` stop or delay until reset rather than looping. See [reliability](https://metaphrase.msalman.com/docs/reliability.md). ## Make Use **Webhooks → Custom webhook** (or your existing trigger), then **HTTP → Make a request**. Select POST, use the same endpoint/headers, body type Raw with content type JSON, and enable response parsing. Map the input text into a JSON-escaped string, then read the parsed `result.text`. Store the Bearer key in a private connection/secret, not an input variable that the browser sends. Configure an error handler for 429/503 with bounded retries and a quota/budget stop condition. These are incoming automation triggers followed by a synchronous API call. MetaPhrase does not send outgoing completion webhooks. A missing-provider 503 requires the MetaPhrase operator's configuration; changing a no-code mapping will not fix it. ## No backend available Use [the embeddable widget](https://metaphrase.msalman.com/docs/widget.md) instead of putting a secret key into a browser-only API connector. The widget script contains only a public widget ID and can be pasted into a custom HTML component. --- # Changelog and versioning REST v1 uses explicit versioned paths and a stable snake_case contract. ## 1.0.0 — initial API contract - Six POST endpoints under `/api/v1`: paraphrase, grammar, detect, translate, summarize and cite. - Bearer keys, per-key rates/UTC monthly quotas, usage accounting and revocation. - SSE for paraphrase rewrites, translation and summarization; consistent JSON errors and exact fix links. - Grammar/citation/extractive summaries with zero LLM token cost. - Public OpenAPI 3.1, Markdown pages, llms.txt/full, language samples, SDK archives, Postman and Insomnia exports. - Origin-restricted paraphraser widget with a public identifier and no frontend secret key. ## Compatibility policy Breaking changes to request/response meaning, removed fields, renamed fields, changed required parameters or incompatible error behavior require a new major REST path such as `/api/v2`. Additive optional fields, new optional parameters, new enum values and improved wording may be released within v1. Clients should tolerate unknown response fields and handle documented errors by `error.code`, not English message text. New enum values should be handled with a fallback in UI code. OpenAPI `info.version` and SDK versions describe the contract release. SDK patch releases can fix client behavior without changing the API version. Keep the OpenAPI checksum with generated artifacts. Provider/model choice is runtime configuration and can change wording or model cost without a new API version; the response always reports the provider/model used. Plans and deployment limits can change; runtime OpenAPI input limits and response rate/quota headers are authoritative. Deprecated endpoints will remain documented with a migration guide and a dated notice before removal. No prior versions or deprecation dates exist yet. This is an early access deployment with Free/Pro placeholders and no payments.