For businesses and developers
Connect your AI assistant
Let customers chat with your AI assistant right here in FastReply. Your AI keeps running on your own servers: we deliver each customer message to it and show its answer in the chat.
- Works with any AI or chatbot
- A developer can set it up in an afternoon
- Your WhatsApp stays as it is
How it works
- A customer writes to your business in FastReply.
- FastReply sends the message to your bot's web address (the endpoint), signed so your bot knows it came from us.
- Your bot asks your AI for an answer.
- Your bot sends the answer to the FastReply API, and the customer sees it in the chat.
Your bot doesn't have to answer inside the same request: it acknowledges the message at once and replies when the AI is ready. Slow AI models are fine.
Security and your data
FastReply is the chat app your customers use, so it receives their messages and passes them on to your endpoint, as any chat or help-desk platform does. Messages aren't end-to-end encrypted: FastReply can technically read them. Here is what we see, what we keep and how we protect it.
- What your bot receives
- The message text, a pseudonymous customer reference (stable for your business, different for every other business) and the customer's language. Their name only if they choose to share it in that chat. Never their email address.
- What we keep, and for how long
- Messages are stored so customers can see their chat history, and deleted after 12 months. When a customer deletes a chat or their account, we delete its messages at once and send you
conversation.closedso you delete your copy. Daily database backups are kept for 14 days, so deleted data is gone from them within two weeks. - Who can read conversations
- The customer and your endpoint. In the dashboard, your team sees only its own “Try it” test chats. Our moderators see a message only when a customer reports it, with the message before it for context. The people who run FastReply can technically reach the database; we open a conversation only to handle a report, a support request or a legal obligation. We don't sell messages or use them for advertising or to train AI.
- In transit
- Everything travels over HTTPS. We call your endpoint only over HTTPS, never follow redirects, refuse private and internal network addresses, and give up after 10 seconds.
- Proof that it's us
- Every event is signed with HMAC-SHA256 and a timestamp, so your endpoint can reject requests we didn't send and replays older than 5 minutes. How to check the signature.
- Your keys
- We store only a hash of your API key: nobody at FastReply can see it, and we can't show it to you again. If you lose it, replace it. Your signing secret is encrypted in our database (AES-256-GCM). Test keys reach only test chats, and live keys only real ones.
- Where the data lives
- In the European Union: the servers, the database and its backups.
- Found a security problem?
- Email contact@expressai.bot, and please give us time to fix it before you make it public. Security contacts are also in our security.txt.
For business owners
You don't need to write code yourself. Here's what you need, and what you do in FastReply.
What you need
- An AI assistant or chatbot that can answer your customers: built by your team, an agency or a chatbot platform, with any AI model.
- Someone who can add a small web endpoint to it: your developer, agency or chatbot provider. Send them this page; the developer part starts at “Developer quickstart”.
- Owner access to your business in FastReply.
What you do in the dashboard
- Open your dashboard, choose your business and go to In-app chat. Paste the endpoint URL your developer gives you and save it. We show a signing secret once: pass it to your developer safely.
- Press Verify now. We send a test event; when your bot answers correctly the connection is verified.
- Accept the data processing terms and press Get API key. Pass the key to your developer: your bot uses it to reply to customers.
- Press Get test key and open a test chat. Chat with your bot as a customer would; only your team sees these chats.
Treat the API key and the signing secret like passwords: share them only with the people who set up your bot, never by public chat or email lists. If one leaks, replace it under Keys and connection; the old one stops working immediately.
FastReply never automates WhatsApp. Customers can still message you there as before; in-app chat is an extra way to reach your AI.
Developer quickstart
You'll add one HTTPS endpoint that receives signed events from FastReply, and call the FastReply API to reply. Any language or framework works. The examples use Python; a full Node.js bot is in “Complete example”.
1. Create an HTTPS endpoint
Add a URL to your server that accepts POST requests with a JSON body, for example https://your-bot.example/fastreply. It must use https://, be reachable from the internet and answer directly: redirects and private or internal addresses are refused.
Developing on your laptop? Expose your local server with a tunnel such as Cloudflare Tunnel or ngrok, and use its https:// address as the endpoint.
2. Check that each event comes from FastReply
Every event carries three headers. Compute an HMAC-SHA256 of timestamp.raw_body with your signing secret and compare it with the signature. Answer 401 if it doesn't match or the timestamp is more than 5 minutes old.
| Header | Meaning |
|---|---|
| X-FastReply-Event-Id | Unique event id. Retries reuse it, so use it to ignore duplicates. |
| X-FastReply-Timestamp | Unix time in seconds when we signed the event. |
| X-FastReply-Signature | v1= followed by the hex HMAC-SHA256 of timestamp.raw_body. |
import hashlib, hmac, time
def valid_signature(raw_body: bytes, timestamp: str, signature: str, secret: str) -> bool:
# Reject missing or old timestamps (more than 5 minutes) to stop replayed events.
if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300:
return False
signed = f"{timestamp}.".encode() + raw_body # the raw bytes, not re-encoded JSON
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(signature.removeprefix("v1="), expected)Sign the raw request body, exactly as received. Parsing the JSON and encoding it again changes the bytes, and the signature won't match.
3. Answer the verification challenge
When you save or verify your endpoint, we send an endpoint.verify event. Check its signature, then answer 200 with the same challenge value in a JSON body.
{
"type": "endpoint.verify",
"id": "evt_01J...",
"created_at": "2026-09-25T10:00:00Z",
"data": { "challenge": "b3f1c9..." }
}HTTP/1.1 200 OK
Content-Type: application/json
{ "challenge": "b3f1c9..." }4. Receive customer messages
Each customer message arrives as a message.created event:
{
"type": "message.created",
"id": "evt_01J...",
"created_at": "2026-09-25T10:01:02Z",
"data": {
"business_id": "biz_01J...",
"conversation": { "id": "conv_01J...", "is_new": false, "is_test": false },
"customer": { "ref": "cus_7f3a...", "locale": "es-ES", "display_name": null },
"message": { "id": "msg_01J...", "type": "text", "text": "Do you have a table for 2 tonight?" }
}
}- Answer 2xx within 10 seconds, before calling your AI. Do the AI work in the background and send the reply in step 5.
- If we don't get a 2xx in time, we retry after 2 s, 5 s, 15 s, 1 min and 5 min with the same event id, then give up and offer the customer WhatsApp instead.
- Keep each chat's history keyed by
conversation.id.customer.refis a stable pseudonym;display_nameis only filled if the customer chose to share their name. conversation.is_testis true for test chats from your dashboard. Answer those with your test key (fr_test_…).
5. Send the reply
Post your AI's answer to the conversation with your API key in the Authorization header. Use the live key (fr_live_…) for customers and the test key for test chats.
curl -X POST "https://api.fastreply.online/v1/business/conversations/conv_01J.../messages" \
-H "Authorization: Bearer $FASTREPLY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"text": "I can hold a table for 2 at 19:30 or 21:00. Which one?",
"reply_to": "msg_01J...",
"quick_replies": ["19:30", "21:00"],
"client_message_id": "msg_01J..."
}'| Field | Rules |
|---|---|
| text | Required. Plain text, 1 to 4,000 characters. HTML and Markdown are shown as typed. |
| reply_to | Optional. The customer message id you're answering; we use it to measure your reply time. |
| quick_replies | Optional. Up to 3 tappable suggestions of up to 25 characters each. |
| sender | bot (default) or staff when a person on your team is writing. |
| client_message_id | Optional. Sending the same value again within 24 hours returns the original message instead of a duplicate, so retries are safe. |
To check a key, call /v1/business/me: it returns your business id, the connection status and whether the key is live or test.
curl "https://api.fastreply.online/v1/business/me" \
-H "Authorization: Bearer $FASTREPLY_API_KEY"6. Connect and test
Deploy your endpoint, then connect it in the dashboard: save the URL and store the signing secret, verify, get the API key, then get a test key and open a test chat. When the test chat works, customers can chat with your bot.
Complete example
A small, working bot. Put your keys in environment variables, replace ask_your_ai with a call to your AI, deploy, and set its /fastreply URL as your endpoint.
FASTREPLY_SIGNING_SECRET=whsec_... # shown once when you save the endpoint
FASTREPLY_API_KEY=fr_live_... # answers real customers
FASTREPLY_TEST_KEY=fr_test_... # answers "Try it" chats from your dashboardPython (FastAPI)
# bot.py: pip install fastapi uvicorn httpx; run with: uvicorn bot:app --port 8080
import hashlib, hmac, json, os, time
import httpx
from fastapi import BackgroundTasks, FastAPI, Request, Response
from fastapi.responses import JSONResponse
API_URL = "https://api.fastreply.online"
SIGNING_SECRET = os.environ["FASTREPLY_SIGNING_SECRET"]
LIVE_KEY = os.environ["FASTREPLY_API_KEY"]
TEST_KEY = os.environ.get("FASTREPLY_TEST_KEY", "")
app = FastAPI()
seen_events: set[str] = set() # use your database or Redis in production
def ask_your_ai(text: str, conversation_id: str) -> str:
# Call your AI here (any model or chatbot platform) and return plain text.
# Use conversation_id to keep each customer's chat history.
return f"Thanks! You said: {text}"
def valid_signature(raw_body: bytes, timestamp: str, signature: str) -> bool:
if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300:
return False
signed = f"{timestamp}.".encode() + raw_body
expected = hmac.new(SIGNING_SECRET.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(signature.removeprefix("v1="), expected)
def answer(event: dict) -> None:
conversation, message = event["data"]["conversation"], event["data"]["message"]
key = TEST_KEY if conversation["is_test"] else LIVE_KEY
path = f"/v1/business/conversations/{conversation['id']}"
with httpx.Client(base_url=API_URL, headers={"Authorization": f"Bearer {key}"}, timeout=10) as api:
api.post(f"{path}/typing") # shows "typing..." while your AI thinks
text = ask_your_ai(message["text"], conversation["id"])
api.post(f"{path}/messages", json={
"text": text[:4000],
"reply_to": message["id"],
"client_message_id": message["id"], # safe to retry
}).raise_for_status()
@app.post("/fastreply")
async def webhook(request: Request, background: BackgroundTasks) -> Response:
raw_body = await request.body()
timestamp = request.headers.get("X-FastReply-Timestamp", "")
signature = request.headers.get("X-FastReply-Signature", "")
if not valid_signature(raw_body, timestamp, signature):
return Response(status_code=401)
event = json.loads(raw_body)
if event["type"] == "endpoint.verify":
return JSONResponse({"challenge": event["data"]["challenge"]})
if event["id"] in seen_events: # a retry we already handled
return Response(status_code=204)
seen_events.add(event["id"])
if event["type"] == "message.created":
background.add_task(answer, event) # runs after we acknowledge
elif event["type"] == "conversation.closed":
pass # delete anything you stored for event["data"]["conversation"]["id"]
return Response(status_code=202)Node.js (Express)
// bot.mjs: npm install express; run with: node bot.mjs (Node 18 or newer)
import crypto from "node:crypto";
import express from "express";
const API_URL = "https://api.fastreply.online";
const { FASTREPLY_SIGNING_SECRET, FASTREPLY_API_KEY, FASTREPLY_TEST_KEY } = process.env;
const app = express();
const seenEvents = new Set(); // use your database or Redis in production
async function askYourAi(text, conversationId) {
// Call your AI here (any model or chatbot platform) and return plain text.
// Use conversationId to keep each customer's chat history.
return `Thanks! You said: ${text}`;
}
function validSignature(rawBody, timestamp = "", signature = "") {
if (!/^\d+$/.test(timestamp) || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const expected = crypto
.createHmac("sha256", FASTREPLY_SIGNING_SECRET)
.update(`${timestamp}.`)
.update(rawBody)
.digest("hex");
const given = signature.replace(/^v1=/, "");
return given.length === expected.length && crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected));
}
async function answer(event) {
const { conversation, message } = event.data;
const key = conversation.is_test ? FASTREPLY_TEST_KEY : FASTREPLY_API_KEY;
const call = (action, body) =>
fetch(`${API_URL}/v1/business/conversations/${conversation.id}/${action}`, {
method: "POST",
headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
body: body ? JSON.stringify(body) : undefined,
});
await call("typing"); // shows "typing..." while your AI thinks
const text = await askYourAi(message.text, conversation.id);
const response = await call("messages", {
text: text.slice(0, 4000),
reply_to: message.id,
client_message_id: message.id, // safe to retry
});
if (!response.ok) console.error("reply failed", response.status, await response.text());
}
// express.raw keeps the exact bytes we signed.
app.post("/fastreply", express.raw({ type: "*/*" }), (req, res) => {
const rawBody = Buffer.isBuffer(req.body) ? req.body : Buffer.alloc(0);
if (!validSignature(rawBody, req.get("X-FastReply-Timestamp"), req.get("X-FastReply-Signature"))) {
return res.sendStatus(401);
}
const event = JSON.parse(rawBody.toString("utf8"));
if (event.type === "endpoint.verify") return res.json({ challenge: event.data.challenge });
if (seenEvents.has(event.id)) return res.sendStatus(204); // a retry we already handled
seenEvents.add(event.id);
if (event.type === "message.created") answer(event).catch(console.error); // after we acknowledge
// conversation.closed: delete anything you stored for event.data.conversation.id
res.sendStatus(202);
});
app.listen(8080);Keep the AI call where the example puts it: after the event is acknowledged. A reply that takes longer than 10 seconds is fine, but a delivery answered after 10 seconds counts as failed.
Before you go live
- Check the signature of every event, and reject timestamps older than 5 minutes.
- Answer 2xx within 10 seconds; call your AI afterwards.
- Ignore events whose
X-FastReply-Event-Idyou've already handled. - Reply to test chats (
is_test: true) with the test key and to customers with the live key. - Send plain text of up to 4,000 characters, and at most 3 quick replies.
- Call
/typingwhile your AI works, so the customer sees “typing…”. - When the AI can't help, call
/handoffand let a person answer withsender: staff. - On
conversation.closed, delete what you stored about that conversation. - Keep the API key and signing secret out of your code repository and logs.
- Stay under 50 requests per second per key; after a
429, wait for the seconds inRetry-After.
Troubleshooting
Failed deliveries from the last 7 days are listed under Delivery errors in your dashboard, with the reason.
- Verification failed: it rejected our signature
- Check that the bot uses the latest signing secret, and that it signs the raw request body rather than re-encoded JSON.
- No answer within 10 seconds
- Make sure the endpoint is deployed and reachable from the internet, and that it answers before calling your AI.
- It redirected
- Use the final address, for example with or without a trailing slash or
www, exactly as your server expects it. - It points to a private or internal address
- Localhost and private network addresses can't be reached. Deploy the bot or use a tunnel.
- It didn't echo the challenge
- Answer
endpoint.verifywith status 200 and a JSON body holding the samechallengevalue. - 401 invalid_api_key
- The key is wrong or was replaced. Copy the current key into your bot's settings.
- 404 conversation_not_found
- Usually the wrong kind of key: test chats only accept the test key, and customer chats only the live key.
- 403 blocked, integration_disabled or conversation_not_owned
- The customer blocked your business, the connection is disabled, or the conversation belongs to another business. Don't retry.
- Customers are offered WhatsApp instead
- After 20 failed deliveries in a row, in-app chat pauses until your bot answers again. Fix the error shown in the dashboard and verify again.
API reference
Base URL: https://api.fastreply.online. Authenticate every request with Authorization: Bearer and your API key.
Business API
| Request | What it does |
|---|---|
| POST /v1/business/conversations/{id}/messages | Send a reply. Returns 201 with the message id. |
| POST /v1/business/conversations/{id}/typing | Show “typing…” for up to 8 seconds, or until your reply arrives. Returns 204. |
| POST /v1/business/conversations/{id}/handoff | Hand the chat to your team, with an optional note of up to 500 characters. The customer sees that a team member will reply. Returns 204. |
| GET /v1/business/me | Check your key: business id, connection status, key prefix and mode (live or test). |
Events we send to your endpoint
| Event | When |
|---|---|
| endpoint.verify | You save or verify your endpoint. Echo the challenge. |
| message.created | A customer sent a message. |
| conversation.closed | The customer deleted the chat or their account, or you disconnected your bot. Delete the conversation's data. |
Errors
Errors come as JSON with a code and a message inside error. Codes: invalid_request, text_too_long, too_many_quick_replies (400), invalid_api_key (401), integration_disabled, conversation_not_owned, blocked (403), conversation_not_found (404), conversation_closed (409) and rate_limited (429).
Stuck, or want us to review your setup? Write to us at contact@expressai.bot.