# 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 <key>` 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
<?php
declare(strict_types=1);
$key = getenv('METAPHRASE_API_KEY');
if (!$key) { throw new RuntimeException('Set METAPHRASE_API_KEY on your backend'); }
$base = rtrim(getenv('METAPHRASE_BASE_URL') ?: 'https://metaphrase.msalman.com', '/');
$payload = json_decode('{"text":"Good writing helps people understand an idea.","mode":"standard","synonym_strength":50,"action":"rewrite","stream":false}', true, 512, JSON_THROW_ON_ERROR);
$curl = curl_init($base . '/api/v1/paraphrase');
curl_setopt_array($curl, [
    CURLOPT_POST => 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<String> 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<void> 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);
  }
}
```
