← All docs

Developers

ReplyX Commerce Protocol

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

Who this is for

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.

Machine-readable spec: https://replyx.io/commerce-protocol/openapi.json — OpenAPI 3.1, importable into Postman, Insomnia or a codegen.

Day one: three endpoints

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.

1

Say what you support

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 } }
2

Answer what you sell

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" } ] }
3

Answer where an order is

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" } } ] }
That is a working integration. Your merchants get catalogue answers and order tracking across WhatsApp, Messenger, Instagram, Telegram, email and their website. Taking orders inside the chat is a second, optional step — POST /orders, further down.

Conventions

  • HTTPS only. ReplyX refuses plain http, and refuses any host that resolves to a private address.
  • Money is always an integer in minor units. 2,490 taka is 249000 — never a float, never a formatted string. The AI compares amounts to a customer's budget and cannot do that with ৳2,490.
  • Answer 200 with a JSON object. An unknown customer is an empty array, not a 404. Use a 4xx with an error object only for a genuine client error.
  • ReplyX gives up after 10 seconds. Catalogue search happens while a customer waits in chat.
  • Times are ISO 8601 with a timezone. Text is UTF-8 — Bangla product names are normal here.
  • Unknown fields are ignored, so you may return more than the spec lists.

GET /capabilities

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.

1

Report your version and what you support

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 } ] }
With neither search_products nor lookup_orders true, ReplyX refuses the connection — there would be nothing for the AI to use.

GET /products

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.

1

Return matching products

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 } ] } ] }

GET /orders

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.

1

Return that customer's orders

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 } } ] }
An order carrying neither phone nor email is discarded. ReplyX only reads an order out to someone whose phone or email matches it, so nobody can read a stranger's order by guessing an order number — with no identity on the order there is nothing to check against.

Signing requests (optional — matters for writes)

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.

1

The signed payload

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

Verify it (PHP)

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
3

Verify it (Node)

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); }
Signing the path alone, or forgetting the query string, is the usual reason a first integration answers 401. If in doubt, accept the Bearer key and log the signature mismatch rather than rejecting — then compare against the payload format above.

POST /orders

Live. ReplyX calls this only when you declare 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.

1

Accept the order

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" } }
2

Answer with what you recorded

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 }

POST /orders/{orderNumber}/cancel

Optional — build it only if you declare 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.

1

Refuse freely

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." } }
2

Be idempotent

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" }

Errors

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.

1

Shape

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." } }
Do not answer 404 for a customer with no orders — that is a 200 with an empty array. A 404 reads as “your integration is broken”, and the AI will tell the merchant so.

Versioning

  • The version is the integer in /capabilities and in the X-ReplyX-Protocol header. Today both are 1.
  • Within a version, only additions happen — new optional fields, new capability flags, new endpoints gated behind a capability. Nothing you have built stops working.
  • A breaking change would ship as version 2, and ReplyX would keep calling version 1 platforms while both exist. You will not be asked to migrate on short notice.
  • Return fields you do not yet support as null or omit them; never invent a value to fill a shape.

Check your work, then ship

1

Run the conformance checker

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.

2

Tell your merchants how to connect

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.

3

Give them a re-check

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.

Building the three read endpoints is enough to be fully usable — your merchants get catalogue search and order tracking on day one. Add POST /orders when you want customers to be able to order inside the chat, and the cancel endpoint alongside it so a change of mind is one message rather than a phone call. Both are live. Shipping-event webhooks arrive in 1.1.

Ready to do it for real?

Everything above is a few clicks away — free to start, no card.

No card required · Free plan forever · Set up in minutes