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

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.

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:

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:

<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>My paraphraser</title>
<style>body{font:16px/1.6 system-ui;max-width:760px;margin:50px auto;padding:20px}textarea{box-sizing:border-box;width:100%;min-height:180px;padding:12px;font:inherit}button{padding:10px 18px;margin-top:14px}pre{white-space:pre-wrap;overflow-wrap:anywhere}#error{color:#a23131}</style></head>
<body><main><h1>A fresh way to say it</h1><form id="form"><label for="text">Your text</label><textarea id="text" required></textarea><button>Paraphrase</button></form><p id="error" role="alert"></p><h2>Result</h2><pre id="result" aria-live="polite"></pre></main>
<script>
const form=document.getElementById('form'), button=form.querySelector('button');
form.addEventListener('submit',async event=>{
  event.preventDefault();button.disabled=true;document.getElementById('error').textContent='';
  try {
    const response=await fetch('/api/metaphrase',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({text:document.getElementById('text').value})});
    const data=await response.json();
    if(!response.ok)throw new Error(data.error?.message??'Please retry.');
    document.getElementById('result').textContent=data.result.text;
  }catch(error){document.getElementById('error').textContent=error.message;}
  finally{button.disabled=false;}
});
</script></body></html>

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.