# 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 <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 |
| `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
<?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":"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}', true, 512, JSON_THROW_ON_ERROR);
$curl = curl_init($base . '/api/v1/detect');
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": "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<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": "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<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":"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);
  }
}
```
