Keep users in the flow
with less typing.
Add autocomplete to the message box you already have. Gray suggestions appear as your user types. Tab accepts one word.
The API predicts the user’s unfinished message, using their draft and recent conversation. Your chatbot continues to handle the reply.
A small, hands-on preview
Install the SDK from npm and request an API key from our team. We approve your website origins and provide API keys directly; no account dashboard is needed. Request access and we’ll help you get started.
Quickstart
Connect Autocomplete to your existing chatbot. Send the setup to your coding agent, or follow the steps yourself.
Let your agent handle the setup.
Copy these instructions into your coding agent. It will work with your app’s existing textbox, backend and login.
The prompt covers installation, authentication, connecting your textbox and checking the integration.
Preview instructions
Integrate HyperWrite Autocomplete into this app's existing chatbot message box: gray inline suggestions for the user's unfinished message, with Tab accepting one word. Preserve the existing interface and send behavior.
Read https://autocomplete.dev/docs and https://autocomplete.dev/openapi.json first. Use the public npm package @hyperwrite/autocomplete and its included README; no private repository access is needed. Adapt the examples to this app's existing stack, authentication and package manager. Keep the integration small.
1. Inspect the composer and login flow. The React wrapper supports React/React DOM 18.2 or 19 and one native or ref-forwarding controlled textarea, including autosizing textareas. For non-React native textareas, use the /dom adapter. For contenteditable or rich-text editors, explain the compatibility gap before changing anything; do not replace the editor or invent support.
2. Configure AUTOCOMPLETE_API_KEY and AUTOCOMPLETE_WEBSITE_ORIGIN on the server. Read the key from the environment; never request it in chat, print it, commit it or expose it to the browser. Use placeholders in .env.example. The website origin must exactly match an approved browser origin, including scheme/port and no trailing slash. Local HTTP localhost origins need explicit approval and a development API key. If configuration is missing, continue independent implementation and list what is needed.
3. Add POST /api/autocomplete-session using this app's verified server-side login, eligibility checks, CSRF protection and rate limiting. Derive a stable opaque user ID from the authenticated internal ID, not an email or request JSON; the docs show SHA-256 mapping. Check the browser's origin against server configuration. Call POST https://autocomplete.dev/v1/browser-sessions with Authorization: Bearer <server API key> and JSON {"user_id": "<opaque ID>", "origin": "<configured origin>"}. Construct fresh upstream headers without forwarding the browser's Origin header. Return only {token, expires_at, expires_in} with Cache-Control: no-store; expires_at is Unix seconds and expires_in is remaining seconds calculated on the server. Use bounded timeouts and safe JSON errors. Do not invent login or expose an anonymous token endpoint.
4. Install @hyperwrite/autocomplete. For React, create a stable createAutocompleteClient from @hyperwrite/autocomplete with baseUrl https://autocomplete.dev. Its getSession calls this app's new route through its authenticated/CSRF-aware request helper. The SDK handles direct browser predictions and token renewal. Wrap only the existing textarea in Autocomplete from @hyperwrite/autocomplete/react. Pass value, onValueChange, messages, contextKey and getSuggestions={client.getSuggestions}. Keep the child's onChange for normal typing; onValueChange commits accepted suggestions. Both use the same controlled draft. Preserve styling, refs, keyboard handlers, autosizing, attachments and voice controls; keep buttons outside the wrapper.
5. Pass recent user/assistant turns as {role, content}, with the unfinished draft only in value. Change contextKey when the user or conversation changes. Keep a client per user/composer, warm its session when eligible, and call resetSession() on cleanup/logout, not dispose() in React StrictMode cleanup. Clear or unmount the draft on account changes. Pause with enabled={false} when appropriate. Do not add persistent storage or logging of drafts, history, predictions or credentials.
6. Run relevant checks and deterministic integration tests. Verify signed-out requests are rejected, typing/sending survive failures, and the browser never receives the API key. With credentials and an approved origin, confirm the session route returns 200 and browser predictions call /v1/autocomplete directly. In a new conversation, type at least 40 characters followed by a space; an assistant welcome alone is not a previous user turn. Confirm gray text and one-word Tab acceptance. Empty suggestions are valid. Check user/conversation switching and existing composer controls. Distinguish mocked tests from live checks; report anything unverified.
Finish with the changed files, required configuration, checks and remaining blockers. Leave a reviewable diff; do not deploy automatically.
Your API key stays in your server environment. The instructions tell your agent to read it there, never from chat.
Need an API key or an origin approved? Request access. Already set up? Give the prompt to your agent and review its changes.
Use React 18 or 19 and an existing controlled textarea. You’ll add a small session route to your backend, then wrap your input.
1 Install the SDK
Install the package in your frontend project. Request access for an API key and approval of your exact website origins.
npm install @hyperwrite/autocomplete
Includes TypeScript definitions. React and React DOM 18.2 or 19 are required for the React integration; the JavaScript core does not require React.
2 Configure your backend
Set these environment variables in your backend’s hosting settings. Use your API key and your exact approved website origin.
AUTOCOMPLETE_API_KEY=YOUR_API_KEY
AUTOCOMPLETE_WEBSITE_ORIGIN=https://chat.example.com
An origin includes the scheme and hostname, plus a port when needed, with no trailing slash. For local development, request approval for an origin such as http://localhost:3000.
For local development, load these values through your backend’s environment configuration. If you use a .env file, keep it gitignored. Keep the API key out of frontend environment variables and source control.
3 Add your session route
Choose your backend language. Your frontend will call /api/autocomplete-session. This route calls https://autocomplete.dev/v1/browser-sessions, which we already host. Both examples work with the React code in the next step.
Add this route to your existing Python 3.11+ / FastAPI backend, where app is your FastAPI app. Install the HTTP client in your backend environment with pip install httpx.
Connect your existing login here
The marked request.state.user_id lookup must read the user from your verified server-side login. FastAPI and our SDK do not populate this value. Adapt that line to your auth middleware or dependency; never fill it from request JSON. Keep your app’s authentication, CSRF and rate-limit checks on this route, and apply any plan or experiment eligibility checks.
import asyncio
import hashlib
import os
import time
import httpx
from fastapi import Request
from fastapi.responses import JSONResponse
# Add to your existing FastAPI app. Load these on the server only.
api_key = os.environ.get("AUTOCOMPLETE_API_KEY")
website_origin = os.environ.get("AUTOCOMPLETE_WEBSITE_ORIGIN")
if not api_key or not website_origin:
raise RuntimeError("Set AUTOCOMPLETE_API_KEY and AUTOCOMPLETE_WEBSITE_ORIGIN")
def session_response(body, status=200):
return JSONResponse(body, status_code=status, headers={"Cache-Control": "no-store"})
# Keep your existing login, CSRF and rate-limit middleware on this route.
@app.post("/api/autocomplete-session")
async def autocomplete_session(request: Request):
# CONNECT YOUR AUTH: this value must come from your verified login.
# Adapt this line to your auth library's stable internal user ID.
user_id = getattr(request.state, "user_id", None)
if user_id is None or user_id == "":
return session_response({"error": "sign_in_required"}, 401)
# Apply any plan or experiment eligibility checks here as well.
if request.headers.get("origin") != website_origin:
return session_response({"error": "origin_not_allowed"}, 403)
# Map your internal ID (not an email) to an API-compatible opaque ID.
opaque_user_id = hashlib.sha256(str(user_id).encode("utf-8")).hexdigest()
try:
started_at = time.monotonic()
async with asyncio.timeout(5), httpx.AsyncClient(timeout=5, follow_redirects=False) as client:
response = await client.post(
"https://autocomplete.dev/v1/browser-sessions",
headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
json={"user_id": opaque_user_id, "origin": website_origin},
)
if not response.is_success:
return session_response({
"error": "autocomplete_unavailable", "upstream_status": response.status_code,
}, 429 if response.status_code == 429 else 502)
session = response.json()
token, expires_at = session.get("token"), session.get("expires_at")
if (not isinstance(token, str) or not token or type(expires_at) is not int
or not 0 < expires_at <= 9_007_199_254_740_991):
return session_response({"error": "invalid_session_response"}, 502)
if "expires_in" in session:
expires_in = session["expires_in"]
if type(expires_in) not in (int, float) or not 0 <= expires_in <= 900:
return session_response({"error": "invalid_session_response"}, 502)
expires_in -= max(0, time.monotonic() - started_at)
else: # Compatibility with older APIs; uses only the server's clock.
expires_in = min(900, expires_at - time.time())
if expires_in <= 0:
return session_response({"error": "invalid_session_response"}, 502)
return session_response({"token": token, "expires_at": expires_at, "expires_in": expires_in})
except (httpx.HTTPError, TimeoutError, ValueError, AttributeError):
return session_response({"error": "autocomplete_unavailable"}, 503)
If you use Uvicorn locally, uvicorn server:app --env-file .env loads your environment file when python-dotenv is installed (included in uvicorn[standard]). Replace server:app with your existing app’s module and name.
Add this route to your existing Node 22+ / Express backend, where app is your Express app.
Connect your existing login here
The marked req.user?.id line must read the user from your verified server-side login. Express and our SDK do not create req.user for you. Adapt that line to your auth library; never fill it from request JSON. Register this route after your app’s authentication, CSRF and rate-limit middleware, and apply any plan or experiment eligibility checks.
import { createHash } from 'node:crypto';
// Add to your existing Express app. Load these on the server only.
const apiKey = process.env.AUTOCOMPLETE_API_KEY;
const websiteOrigin = process.env.AUTOCOMPLETE_WEBSITE_ORIGIN;
if (!apiKey || !websiteOrigin) {
throw new Error('Set AUTOCOMPLETE_API_KEY and AUTOCOMPLETE_WEBSITE_ORIGIN');
}
// Register after your existing login, CSRF and rate-limit middleware.
app.post('/api/autocomplete-session', async (req, res) => {
res.set('Cache-Control', 'no-store');
// CONNECT YOUR AUTH: req.user must come from your verified login.
// Adapt this line to your auth library's stable internal user ID.
const userId = req.user?.id;
if (userId == null || userId === '') {
return res.status(401).json({ error: 'sign_in_required' });
}
// Apply any plan or experiment eligibility checks here as well.
if (req.get('Origin') !== websiteOrigin) {
return res.status(403).json({ error: 'origin_not_allowed' });
}
// Map your internal ID (not an email) to an API-compatible opaque ID.
const opaqueUserId = createHash('sha256').update(String(userId)).digest('hex');
try {
const startedAt = performance.now();
const response = await fetch('https://autocomplete.dev/v1/browser-sessions', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ user_id: opaqueUserId, origin: websiteOrigin }),
signal: AbortSignal.timeout(5000),
redirect: 'error',
});
if (!response.ok) {
return res.status(response.status === 429 ? 429 : 502).json({
error: 'autocomplete_unavailable', upstream_status: response.status,
});
}
const session = await response.json();
const { token, expires_at } = session;
if (typeof token !== 'string' || !token ||
!Number.isSafeInteger(expires_at) || expires_at <= 0) {
return res.status(502).json({ error: 'invalid_session_response' });
}
let expires_in;
if (Object.hasOwn(session, 'expires_in')) {
expires_in = session.expires_in;
if (!Number.isFinite(expires_in) || expires_in < 0 || expires_in > 900) {
return res.status(502).json({ error: 'invalid_session_response' });
}
expires_in -= Math.max(0, performance.now() - startedAt) / 1000;
} else { // Compatibility with older APIs; uses only the server's clock.
expires_in = Math.min(900, expires_at - Date.now() / 1000);
}
if (expires_in <= 0) return res.status(502).json({ error: 'invalid_session_response' });
return res.json({ token, expires_at, expires_in });
} catch {
return res.status(503).json({ error: 'autocomplete_unavailable' });
}
});
For a local .env file, run node --env-file=.env server.mjs, using your server’s entry filename.
Use a stable internal user ID unique across your project, not an email. The example hashes it into a consistent API-compatible ID, including when your login provider uses characters such as |.
This example serves /api/autocomplete-session on the same origin as your frontend. If your local frontend and backend use different ports, proxy /api to your backend in your dev server. AUTOCOMPLETE_WEBSITE_ORIGIN is the website address in the browser, not your backend’s address.
4 Connect your existing textbox
In the React example below, getSession calls the route you just added. The browser sends its existing login credentials; the API key stays on your server. The route returns a browser token, and the SDK uses it for prediction requests directly to Autocomplete.
The client handles API requests and session renewal. Autocomplete handles the suggestions, matching queue, and keyboard behavior. Initialize the client once per user/composer.
import { useEffect, useMemo, useState } from 'react';
import {
createAutocompleteClient,
type ChatMessage,
} from '@hyperwrite/autocomplete';
import { Autocomplete } from '@hyperwrite/autocomplete/react';
type Props = {
userId: string;
conversationId: string;
messages: ChatMessage[];
onSend: (message: string) => void;
};
export function Composer({
userId, conversationId, messages, onSend,
}: Props) {
const [draft, setDraft] = useState('');
const client = useMemo(() => createAutocompleteClient({
baseUrl: 'https://autocomplete.dev',
async getSession() {
// Calls YOUR backend route from step 3. No API key here.
// Use your existing auth/CSRF request helper if your app needs one.
const response = await fetch('/api/autocomplete-session', {
method: 'POST',
credentials: 'same-origin',
cache: 'no-store',
signal: AbortSignal.timeout(6000),
});
if (!response.ok) throw new Error('Session unavailable');
return response.json();
},
}), [userId]);
useEffect(() => {
void client.warmSession().catch(() => {});
return () => client.resetSession();
}, [client]);
return (
<div>
<label htmlFor="message">Your message</label>
<Autocomplete
value={draft}
onValueChange={setDraft}
contextKey={`${userId}:${conversationId}`}
messages={messages}
getSuggestions={client.getSuggestions}
>
<textarea
id="message"
value={draft}
onChange={event => setDraft(event.target.value)}
/>
</Autocomplete>
<button
type="button"
disabled={!draft.trim()}
onClick={() => { onSend(draft); setDraft(''); }}
>
Send
</button>
</div>
);
}
Pass messages as recent { role: 'user' | 'assistant', content: string } turns. Keep the unfinished draft in value. Change contextKey when the user or conversation changes; the SDK clears incompatible suggestions.
Mount this example with key={`${userId}:${conversationId}`} on Composer so switching users or chats also resets its local draft. contextKey resets suggestions, not your application’s draft state.
Keep normal typing in your existing handler. The textarea’s onChange handles typing. onValueChange commits accepted suggestions only. Give both the wrapper and textarea the same controlled value.
5 Check the connection
- Open your app on the approved origin and sign in with a user eligible for autocomplete.
- Open your browser’s Network panel. When the composer mounts,
POST /api/autocomplete-sessionshould return200with the two fields below. - Focus the textbox. In a new conversation, type a message of at least 40 characters followed by a space. Look for a request to
https://autocomplete.dev/v1/autocomplete. When a gray suggestion appears, press Tab to accept one word.
{
"token": "GENERATED_BROWSER_TOKEN",
"expires_at": 1900000600,
"expires_in": 600
}
Illustrative response. The actual browser token and expiry are generated for each session; expires_at is Unix seconds and expires_in is remaining seconds calculated on the server. Sessions last 10 minutes by default, and the SDK handles renewal.
Your API key should appear only in your backend’s request to our API. The frontend receives only the browser token. An autocomplete response with no candidates is normal; the user can keep typing.
Common setup errors
| Response | What to check |
|---|---|
401 · sign_in_required | Sign in and connect the marked user ID line to your verified login. Make sure your frontend sends the cookie or authorization headers your app expects. |
403 · origin_not_allowed | The browser’s origin must exactly match AUTOCOMPLETE_WEBSITE_ORIGIN. Check the scheme and port; omit the trailing slash. |
502 · upstream_status: 401 | The API key is incorrect or revoked. Check the server environment value. A missing environment variable stops the example at startup. |
502 · upstream_status: 403 | Ask us to approve the configured website origin. Keep the example’s upstream headers; do not forward the browser’s Origin header with the API key. |
502 · upstream_status: 422 | Check the upstream request format and origin. Use the example’s stable user ID mapping; don’t send an email or raw request-body identity. |
429 | A rate limit was reached. Wait before trying again; don’t retry in a tight loop. |
Other 502 or 503 | The upstream service failed, timed out or returned an invalid response. Check connectivity and contact us if it persists. Normal typing should keep working. |
If the session request returns 404 or an HTML page, check that the route is registered and your frontend’s /api proxy reaches it. Your existing auth middleware should return JSON errors for this route instead of redirecting to a login page.
Authentication
Your API key is the secret we provide during onboarding. It stays on your backend. Your frontend uses a short-lived browser token to request predictions directly from Autocomplete.
Your backend is involved at session creation and renewal, not on every keystroke. A serverless function works too.
The backend’s request
POST https://autocomplete.dev/v1/browser-sessions
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
{
"user_id": "opaque-user-123",
"origin": "https://chat.example.com"
}
Return token, expires_at, and expires_in to your browser, with Cache-Control: no-store. expires_at is Unix seconds; calculate remaining seconds expires_in on the server so browser clock differences do not discard valid sessions. Sessions last 10 minutes by default. The SDK keeps the token in memory and handles renewal.
- Authenticate the signed-in user and check whether they’re eligible for autocomplete.
- Derive a stable, opaque
user_idfrom that session. Don’t trust an ID supplied by the browser or use an email address. - Use an approved, configured origin such as
https://chat.example.com. Origins must match exactly, including any non-default port, with no path or wildcard. - Rate-limit your route and retain your app’s normal authentication and CSRF protection.
Our team issues and revokes API keys with our operator tool, then delivers them securely during onboarding. Customers do not generate production API keys from a public terminal command. Never include your API key in frontend code, browser storage, or a public repository.
Your textbox stays yours
Autocomplete accepts one controlled textarea child. It keeps your styles, ref, and event handlers, and adds no layout wrapper. Put labels, send buttons, file uploads, and voice controls outside the component.
Using an autosizing textarea? Keep it. Custom components must forward their ref and standard textarea props to the native element. Voice input should update the same React state, using your existing ref for caret positioning.
Your keyboard handler runs first. Calling preventDefault() takes precedence over the SDK. Enter remains your app’s Send or newline behavior.
| Key | Action |
|---|---|
| Tab | Accept the next word. |
| → Right Arrow | Accept the remaining suggestion. |
| Alt / Option + ↑ / ↓ | Cycle through alternatives. |
| Esc | Dismiss the suggestion. |
| Shift + Tab | Move focus normally. |
Tab without a suggestion and plain Up/Down retain their normal behavior. The running queue keeps compatible suggestions, so typing or backspacing can reveal a match without waiting for another request.
Supported in this preview
Desktop native and ref-forwarding textareas, including autosizing, with suggestions at the end of the draft. Contenteditable and rich-text editors need additional support. Mobile keyboards, broad multilingual word acceptance, and all browser/accessibility combinations are not yet qualified. Tell us about your editor and we’ll help evaluate it.
Give the input an accessible label. The React component adds keyboard instructions and a suggestion-availability announcement. Suggestions pause during composition, selection, or when the caret moves away from the end.
A few useful options
Start with the defaults. Add these props to Autocomplete when your app needs them.
Give users an easy way to turn suggestions off, and connect that preference to enabled. They can keep typing normally when autocomplete is off.
| Prop | What it does |
|---|---|
enabled | Set false to suspend autocomplete, for example while recording voice input or sending a message. Disabled and read-only textareas also suspend it. |
systemPrompt | Optional writing guidance for the user’s message, such as tone or domain. Up to 2,000 Unicode characters. Empty by default. |
additionalContext | Relevant reference text or document excerpts, most useful first. Up to 12,000 Unicode characters; context may be shortened to fit. Empty by default. |
theme | Adjust suggestion color, opacity, tabBorderColor, and tabBackgroundColor. By default, the overlay follows your textarea’s typography and text color. |
timeBetweenAutocompleteMs | Optional minimum gap between request starts; 0 ms by default. The API enforces its own per-user pacing independently. |
Fresh suggestions are requested after a typed space or accepted word, while matching suggestions remain available. Requests start immediately by default, with no SDK concurrency cap. The API may reject requests inside its per-user cooldown; rejected requests are not retried automatically.
Reference context is text, not a file upload: URLs and filenames are not fetched. Pass relevant excerpts rather than entire documents. Changes to guidance or reference text clear pending work and incompatible suggestions automatically.
Ordinary localhost works
Your app can run at http://localhost:3000 and call the hosted HTTPS API. No local certificates are needed.
- Ask us for a separate development project and API key.
- Have your exact local origin approved, including the port.
- Configure your local backend session route to use that development API key and origin.
- Keep the client’s
baseUrlset tohttps://autocomplete.dev.
localhost and 127.0.0.1 are different origins. Only explicitly approved HTTP loopback origins are allowed; public websites use HTTPS.
The API, at a glance
The SDK takes care of request identifiers, correlation, suffix matching, and expiry. Use the HTTP reference if you’re building a custom client or checking an exact schema.
/v1/browser-sessionsCreate a browser session with your API key.
POST/v1/autocompletePredict a suffix for the current user draft.
POST/v1/eventsSend optional, content-free interaction counts.
Candidates are suffixes to the exact requested prefix. Preserve whitespace: "Can you sum" + "marize this". An empty candidate list is a normal response.
Request limits
- 32 KiB maximum JSON request body.
- 4,000-character draft; up to 12 recent user/assistant messages, each up to 10,000 characters.
- Up to 30 returned candidates, each up to 512 characters.
These are individual bounds, not amounts that fit together in one request. Combined body and model-context limits also apply. The SDK counts text limits in UTF-16 code units; HTTP string limits count Unicode characters. Oversized drafts are rejected rather than silently changed.
Errors shouldn’t interrupt typing
| Status | Meaning and handling |
|---|---|
401 | Invalid or expired credentials. The SDK refreshes the session once. |
403 | Origin or scope mismatch. Check your project’s approved origins and session setup. |
409 | Duplicate request ID. Generate a new ID for each new request. |
413 / 415 / 422 | Request size, content type, schema, or context-limit issue. Correct the request before retrying. |
429 | Requests too close together. The rejected request is not retried automatically. The client exposes Retry-After metadata; later user-triggered requests are not automatically delayed. |
503 / 504 | Service unavailable or inference timeout. Keep typing available; don’t automatically replay the request. |
The API returns { "error": { "code": "…", "message": "…" } }. The SDK does not replay inference after network failures or timeouts.
Data handling
The API processes drafts, conversation text, writing guidance, and suggestions in memory. It does not persist this content in its application storage or production logs. The SDK keeps its suggestion queue and session credentials in memory, with no persistent browser storage or automatic interaction analytics.
Project configuration, key hashes, and content-free operational records are retained. Content is sent to the configured inference provider to generate predictions; this description does not establish that provider’s retention policy. Contact us for current provider-handling details before a production rollout.
Keep drafts, credentials, and suggestions out of your own application logs and monitoring tools.
Let’s make it fit your chatbot
A real person, one email away.
Send us your framework and textbox setup. We’ll help with access and your first integration.