# 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 <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 |
| --- | --- | --- | --- | --- | --- |
| `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
<?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('{"source_type":"manual","style":"apa","fields":{"type":"book","title":"A clear idea","authors":[{"given":"Ada","family":"Lovelace"}],"year":2024,"publisher":"Example Press"},"stream":false}', true, 512, JSON_THROW_ON_ERROR);
$curl = curl_init($base . '/api/v1/cite');
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 = """
{
  "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<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 = """
{
  "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<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'''{"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);
  }
}
```
