Browse documentation
METAPHRASE / RECIPESREST API v1
Open in AI
Open in ChatGPT Open in Claude
Markdown

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 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.

python3 -m venv .venv
. .venv/bin/activate
python -m pip install fastapi uvicorn
uvicorn app:app --host 127.0.0.1 --port 4000
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:

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:

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:

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.