Developers
Implement one documented contract and every shop on your platform can connect its catalogue and orders to a ReplyX AI.
Three read endpoints — an afternoon
You run an e-commerce platform or a custom CMS. ReplyX ships adapters for Shopify and WooCommerce and will not write one per platform — so the contract is published instead: you implement it once, and every merchant on your platform can connect the same day by pasting a base URL and an API key. Nothing is needed from us.
What your merchants get: an AI that answers “do you have this in red under 3,000?” from the live catalogue with real prices, stock and links, and “where is my order?” with real status and tracking — on WhatsApp, Messenger, Instagram, Telegram, email and their website, in Bangla or English.
You host the endpoints; ReplyX calls them. Nothing is copied into ReplyX, so a price change is live in the next answer and your data never sits in our database.
Serve these three at a base URL of your choosing, return JSON, and check Authorization: Bearer <key> against the key your merchant generated. That is the whole of it — no signing, no webhooks, no callbacks, nothing to keep in sync. Everything else on this page is optional and can wait.
Called once when a merchant connects, and again whenever they press Re-check. Anything you do not declare is never requested.
GET /capabilities
{
"protocol": 1,
"store": { "name": "Alpha Store", "currency": "BDT" },
"capabilities": {
"search_products": true,
"lookup_orders": true
}
}Called mid-conversation when a customer describes what they want. Prices are whole paisa as an integer.
GET /products?q=three+piece&limit=5
{
"products": [
{
"id": "SKU-1042",
"title": "Cotton Three Piece — Red",
"priceMinor": 249000,
"inStock": true,
"url": "https://shop.example.com/p/1042"
}
]
}Called when a customer asks about delivery. Include the customer phone or email — it is what ReplyX matches before reading anything out, so an order without one is withheld.
GET /orders?phone=01712345678
{
"orders": [
{
"orderNumber": "A-10421",
"status": "shipped",
"totalMinor": 255000,
"customer": { "phone": "+8801712345678" }
}
]
}249000 — never a float, never a formatted string. The AI compares amounts to a customer's budget and cannot do that with ৳2,490.The first call ReplyX makes, on connect and on every re-check. It decides what the AI will offer, so a catalogue-only platform is never asked to create an order. Anything absent or false is never attempted.
protocol must be 1. delivery_areas is optional but lets the AI quote a delivery charge before an order is placed.
200 Response
{
"protocol": 1,
"store": {
"name": "Alpha Store",
"currency": "BDT"
},
"capabilities": {
"search_products": true,
"lookup_orders": true,
"create_orders": false,
"payment_links": false,
"cancel_orders": false
},
"delivery_areas": [
{
"code": "dhaka",
"label": "Inside Dhaka",
"chargeMinor": 6000
},
{
"code": "outside",
"label": "Outside Dhaka",
"chargeMinor": 12000
}
]
}Called when a customer asks about items. Parameters: q, min_price_minor, max_price_minor, category, limit (never more than 20). Narrowing on your side is faster, but ReplyX filters the result again — ignoring the parameters is slow, not wrong.
title is the only required field — it is what the customer is shown. A missing inStock is read as in stock, so a platform that tracks no inventory still works. List variants when sizes or colours differ in price or availability: ReplyX shows each as its own line, so it can say size L is sold out instead of hiding the product.
200 Response
{
"products": [
{
"id": "SKU-1042",
"title": "Cotton Three Piece - Red",
"priceMinor": 249000,
"currency": "BDT",
"inStock": true,
"stock": 7,
"url": "https://yourstore.com/p/1042",
"imageUrl": "https://yourstore.com/i/1042.jpg",
"variants": [
{
"id": "SKU-1042-M",
"title": "M",
"priceMinor": 249000,
"inStock": true
},
{
"id": "SKU-1042-L",
"title": "L",
"priceMinor": 269000,
"inStock": false
}
]
}
]
}Called for “where is my order?”. Exactly one of order_number, phone or email arrives. Match a phone on its national significant digits: the same customer is saved as +8801712345678 at checkout and writes 01712345678 in chat, and those two strings share no prefix.
orderNumber is required, and so is a customer phone or email. status is free text shown to the customer — pending, processing, shipped, delivered, cancelled all read fine.
200 Response
{
"orders": [
{
"orderNumber": "A-10421",
"status": "shipped",
"placedAt": "2026-08-01T10:20:00Z",
"totalMinor": 255000,
"currency": "BDT",
"items": [
{
"title": "Cotton Three Piece - Red (M)",
"quantity": 1
}
],
"tracking": {
"company": "Pathao",
"number": "PT123456",
"url": "https://track.pathao.com/PT123456"
},
"customer": {
"name": "Rahim",
"phone": "+8801712345678",
"email": null
}
}
]
}Skip this section until you build POST /orders. The three read endpoints are fine on the Bearer key alone — that is a deliberate position, not an oversight: a catalogue is already public on your website, and orders are behind the key.
Where it earns its keep is the write. POST /orders creates something real in your shop, so proving the request came from ReplyX — and not from someone who found the key in a log — is worth the hour. The timestamp also bounds how long a captured request stays replayable.
X-ReplyX-Timestamp is unix seconds. X-ReplyX-Signature is sha256= followed by lowercase hex of HMAC-SHA256(apiKey, payload), where the payload is the timestamp, the method upper-cased, the path including its query string, and the raw body — empty for a GET. Including the query string is what stops a proxy rewriting ?limit=5 into ?limit=99999.
Payload format
{timestamp}.{METHOD} {path}.{body}
1754563200.GET /products?q=three+piece&limit=5.path is relative to the base URL your merchant pasted into ReplyX. If you serve the protocol at https://yourshop.com/replyx, we sign /products?q=saree — not /replyx/products?q=saree. Your framework will hand you the full server path, so strip your mount prefix before verifying. This is the one mistake that fails every request rather than some of them.Reject a timestamp more than 300 seconds from your clock, then compare in constant time — a byte-by-byte comparison leaks the correct signature to anyone willing to measure how long the answer takes.
PHP
<?php
// Verify that a request really came from ReplyX.
function replyx_verify(string $apiKey, string $method, string $path, string $body): bool {
$ts = $_SERVER['HTTP_X_REPLYX_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_REPLYX_SIGNATURE'] ?? '';
if ($ts === '' || $sig === '') return false;
if (abs(time() - (int) $ts) > 300) return false; // replay window
$payload = $ts . '.' . strtoupper($method) . ' ' . $path . '.' . $body;
$expected = 'sha256=' . hash_hmac('sha256', $payload, $apiKey);
return hash_equals($expected, $sig); // constant time
}
// $path must include the query string exactly as received:
// /products?q=three+piece&limit=5
The same algorithm. This is the implementation ReplyX itself signs with, so the two cannot drift apart.
JavaScript
import { createHmac, timingSafeEqual } from "crypto";
export function replyxVerify({ apiKey, headers, method, path, body = "" }) {
const ts = Number(headers["x-replyx-timestamp"]);
const sig = headers["x-replyx-signature"];
if (!sig || !Number.isFinite(ts)) return false;
if (Math.abs(Math.floor(Date.now() / 1000) - ts) > 300) return false;
const payload = `${ts}.${method.toUpperCase()} ${path}.${body}`;
const expected = "sha256=" + createHmac("sha256", apiKey).update(payload).digest("hex");
const a = Buffer.from(expected), b = Buffer.from(sig);
return a.length === b.length && timingSafeEqual(a, b);
}
create_orders and the merchant has separately switched AI ordering on — connecting a shop never starts it taking orders.You price the order, not ReplyX. We send product ids and quantities; you compute the total, apply the delivery charge and reject what is out of stock. An AI must never be the source of a price, and the total the customer is told is only ever quoted back from your response.
What happens before you are called. ReplyX re-reads your /products for every item, refuses anything you did not just return, prices the basket, reads that exact total back to the customer and waits for a clear yes. Only then does this request go out. Two consequences for you: every productId we send came from one of your own responses moments earlier, and a request arriving here means a human already agreed to a figure.
Idempotent on `idempotencyKey`: the same key must return the same order and never create a second one. This is the single most important line in the spec — the key is ReplyX's quote id, so one basket a customer agreed to can only ever become one order in your shop, however the network behaves. Store it and return the first order on a repeat.
Request
{
"idempotencyKey": "rx_01J9ZQ8F2K7M3N",
"customer": {
"name": "Rahim Uddin",
"phone": "+8801712345678",
"email": null
},
"shipping": {
"areaCode": "dhaka",
"address": "House 12, Road 4, Dhanmondi, Dhaka"
},
"items": [
{
"productId": "SKU-1042-M",
"quantity": 1
}
],
"payment": {
"method": "COD"
},
"source": {
"channel": "whatsapp",
"conversationId": "c_8f21"
}
}Return paymentUrl when the customer chose ONLINE and you declared payment_links; null for cash on delivery. The customer pays on your gateway — ReplyX never touches card data and holds no gateway relationship.
200 Response
{
"orderNumber": "A-10421",
"status": "pending",
"totalMinor": 255000,
"currency": "BDT",
"paymentUrl": null
}cancel_orders. Without it the AI tells a customer who changes their mind that someone will call them back, which is a worse experience but a safe one.ReplyX will only ever ask you to cancel an order it placed itself, in the same conversation, within 24 hours. A customer reading out an order number cannot reach this endpoint — we look the number up in our own record of what we placed, so an order that came from your website or from another chat is simply not found.
You know whether the parcel has left; we do not. A 409 with a plain-language message is a perfectly normal answer — the AI relays it and offers a human, rather than telling the customer it is cancelled.
409 Response
{
"error": {
"code": "ALREADY_SHIPPED",
"message": "This order left our warehouse this morning, so it can no longer be cancelled."
}
}Cancelling an order that is already cancelled is a 200, not an error. A retry must never turn into a second refund.
200 Response
{
"orderNumber": "A-10421",
"status": "cancelled"
}Return an error object with a stable machine code and a message written in plain language — the AI may paraphrase the message to the customer, so write it as something a shopper could read.
Known codes: UNAUTHORIZED, NOT_FOUND, OUT_OF_STOCK, INVALID_ITEM, INVALID_ADDRESS, AREA_NOT_SERVED, MINIMUM_ORDER, RATE_LIMITED, INTERNAL. Anything unrecognised is treated as INTERNAL.
400 Response
{
"error": {
"code": "OUT_OF_STOCK",
"message": "SKU-1042-L is out of stock."
}
}/capabilities and in the X-ReplyX-Protocol header. Today both are 1.2, and ReplyX would keep calling version 1 platforms while both exist. You will not be asked to migrate on short notice.null or omit them; never invent a value to fill a shape.Sign in to ReplyX — the free plan is enough — and open Integrations → Run the conformance checker. Point it at your endpoint and it reports every check with the exact field to change. Nothing is saved and no store is connected.
In their ReplyX workspace: Integrations → Other platform, then paste the base URL and the API key your system issued them. ReplyX calls /capabilities immediately and refuses anything that does not answer.
The channel page has a Re-check button that re-reads /capabilities and shows which features are live. If you retire a capability, that is where the merchant finds out — rather than noticing the AI quietly stopped using it.
Everything above is a few clicks away — free to start, no card.
No card required · Free plan forever · Set up in minutes