Mudheef API

Read and answer your store's conversations from your own systems, keep contacts in sync and get an event the moment a shopper asks for a person, rates a chat or places an order, with whether the chat helped.

Authentication

Create a key in the dashboard under Integrations → Automation → API keys. It is shown once, so copy it somewhere safe. Send it as a bearer token on every request:

curl https://store-chat-steel.vercel.app/v1/admin/inbox?status=open \
  -H "Authorization: Bearer sk_..."

A key can do most of what the dashboard can. It cannot manage API keys, connect or remove integrations, rotate the webhook signing secret, change help answers (the questions and answers your store teaches the AI), change the words shoppers are shown or told (the welcome message, quick replies, the offline message, pop-up messages, saved replies, the payment note, the topics that go straight to the team), or start co-browse: those need the dashboard login. A key can still read help answers and reply in the inbox. Revoking a key stops it within a minute. Requests and responses are JSON in UTF-8, and Arabic text is returned as is.

Errors and limits

Errors come back as {"detail": "..."} with a status that says what to do:

StatusMeaning
401The key is missing, wrong or revoked.
403The key can't do this (it needs the dashboard login).
404No such conversation, contact or entry in this store.
422The request body or a parameter isn't valid; detail says which.
429Too many requests in a short time. Wait a few seconds and retry.
503A temporary outage. Retry with a growing delay.

Conversations

RequestWhat it does
GET/v1/admin/inboxConversations, newest first. Filters: status (open or resolved for hand-offs to the team, ai for chats the AI handled alone), channel (web, whatsapp, telegram ...), assignee, label, topic, q (search), visitor_id, sort (newest, waiting), limit (1 to 200).
GET/v1/admin/inbox/{id}The whole thread: the shopper's messages, the AI's answers, team replies and notes.
POST/v1/admin/inbox/{id}/reply{"agent", "message"}. Reaches the shopper on the channel they wrote from (the website chat, WhatsApp, Telegram, Instagram, Messenger). On WhatsApp, Instagram and Messenger, a reply more than 24 hours after the shopper's last message is blocked: the response has channel.sent: false and an error saying why. While the store's test mode is on (the default), channel replies stay in the test outbox and the response has channel.test: true.
POST/v1/admin/inbox/{id}/note{"agent", "text"}. A note only the team sees.
POST/v1/admin/inbox/{id}/assign{"assignee"}, a teammate's name from Settings, or null.
PUT/v1/admin/inbox/{id}/labels{"labels": [...]}, up to 10.
POST/v1/admin/inbox/{id}/resolveMarks it done and asks the shopper to rate the chat.
# answer a shopper from your own tool
curl -X POST https://store-chat-steel.vercel.app/v1/admin/inbox/CONVERSATION_ID/reply \
  -H "Authorization: Bearer sk_..." -H "Content-Type: application/json" \
  -d '{"agent": "Sara", "message": "طلبك خرج للتوصيل اليوم"}'

Contacts

RequestWhat it does
GET/v1/admin/contactsUp to 100 contacts, most recently active first; q searches name, email, phone and tags.
GET/v1/admin/contacts/{visitor_id}One profile with their conversations and the orders the chat helped with.
PATCH/v1/admin/contacts/{visitor_id}{"name", "tags", "notes"}, any of them.

Knowledge

Entries the AI answers from, next to your catalogue: delivery times, return rules, payment options. The store's index is rebuilt after each change, so a new entry is used within a minute.

RequestWhat it does
GET/v1/admin/knowledgeWhat the AI knows: the indexed documents and your entries.
POST/v1/admin/kb{"title", "body"} adds an entry.
PUT/v1/admin/kb/{id}Replaces an entry.
DELETE/v1/admin/kb/{id}Removes an entry.

Analytics and export

RequestWhat it does
GET/v1/admin/analytics?days=30Conversations, answer rate, hand-offs, ratings, top questions and the sales the chat helped with.
GET/v1/admin/export.csv?days=30Every conversation as CSV (opens in Excel with Arabic intact). 6 a minute.

Settings

RequestWhat it does
GET/v1/admin/configGreeting, quick replies, hours, team, labels, webhooks and the rest of the AI agent's settings.
PUT/v1/admin/configThe same object back, changed. Fields you leave out are reset to their defaults, so send what you read.

Webhooks

Mudheef can send store events to any https address: Zapier, Make, n8n or your own server. Add them in the dashboard under Integrations → Automation → Event webhooks, or in the config's webhooks list, each with the events it wants ("*" for all):

"webhooks": [
  {"url": "https://hooks.zapier.com/hooks/catch/...", "events": ["handoff.opened", "rating.received"]},
  {"url": "https://your-server.example.com/mudheef", "events": ["*"]}
]

Each delivery is a POST with this body, sent within seconds of the event. A delivery that fails is not retried, so treat events as notifications and read the API for the full picture.

{
  "event": "handoff.opened",
  "tenant": "your-store-id",
  "at": "2026-11-01T09:30:12+00:00",
  "data": { ... }
}

Event list

EventWhendata
handoff.openedA shopper asks for a person, or the AI hands over.conversation_id, handoff_id, reason, question, contact, topic
conversation.resolvedThe team marks a conversation done.conversation_id, assignee
rating.receivedThe shopper rates the chat.conversation_id, score (1 to 5), comment, assignee
contact.updatedThe team edits a contact.visitor_id, name, email, phone, tags, channel
order.attributedA new order arrives from a connected store platform. assisted says whether the chat helped (the shopper had an AI answer in the 7 days before).order_id, total, currency, visitor_id, assisted
chat.answeredEvery AI reply. Busy stores send many of these.conversation_id, visitor_id, question, kind, channel, cards

Verifying a delivery

Every delivery carries X-StoreChat-Signature: the hex HMAC-SHA256 of the raw body with your webhook secret (Settings → Hand-off alerts, shown once when you create it). Check it before trusting the body. Nothing is sent until the secret exists.

# Python
import hashlib, hmac

def verified(body: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature or "")
// Node.js: use the raw request body, before any JSON parsing
const crypto = require("crypto");

function verified(body, signature, secret) {
  const expected = crypto.createHmac("sha256", secret).update(body).digest("hex");
  return typeof signature === "string" && signature.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}

Connect Zapier or Make

These need no app: the webhooks above are the connection, and no code is needed.

  1. First create the signing secret (Settings → Hand-off alerts). Nothing is sent until it exists, even to Zapier or Make, which don't check it.
  2. Zapier: make a Zap, pick Webhooks by Zapier as the trigger and Catch Hook as the event, and copy the address from the Test tab. It looks like https://hooks.zapier.com/hooks/catch/123456/abcde/. Zapier's webhook triggers need a paid plan.
    Make: in a scenario add Webhooks → Custom webhook → Add, and copy the address. It looks like https://hook.eu1.make.com/abcdefgh12345678.
  3. In the dashboard, go to Integrations → Automation → Event webhooks, add that address, pick the events and save.
  4. Make one happen, for example by asking for a person in your store's chat. Then press Test trigger in Zapier. Make works out the fields from that first delivery.

A handoff.opened delivery looks like this, and the event list above gives the fields for the others:

{
  "event": "handoff.opened",
  "tenant": "your-store-id",
  "at": "2026-11-01T09:30:12+00:00",
  "data": {
    "conversation_id": "c-1",
    "handoff_id": 77,
    "reason": "requested by visitor",
    "question": "Where is my order?",
    "contact": "sara@example.com",
    "topic": "orders",
    "request": null
  }
}

Treat the address like a password: Zapier and Make can't check the signature without code, so anyone who has the address can send to it. A Zap that's turned off answers 404 and that delivery is lost, since failed deliveries aren't retried. When Make's webhook queue is full, it rejects deliveries too.

Add a row to Google Sheets

Connect Google Sheets under Integrations. It uses a small script in your own sheet, so there's no Google Cloud project and no Google review, and Mudheef never gets your Google login.

  1. Open your sheet and go to Extensions → Apps Script. Delete what's there and paste this:
    function doPost(e){var d=JSON.parse(e.postData.contents),s=SpreadsheetApp.getActiveSpreadsheet().getSheets()[0];if(!s.getLastRow())s.appendRow(d.header);s.appendRow(d.row);return ContentService.createTextOutput('ok')}function doGet(){return ContentService.createTextOutput('ok')}
  2. Deploy → New deployment → Web app. Set Execute as to Me and Who has access to Anyone, then press Deploy and allow it.
  3. Copy the Web app URL, which ends in /exec, into Mudheef.

Each hand-off, resolved chat, contact, order and rating then adds a row to the first tab. Anyone with that address can add rows to this sheet, so keep it private, as you would a password.

Built-in integrations

Many tools need no code at all: connect them in the dashboard under Integrations with your own account details. Channels (WhatsApp, Instagram, Messenger, Telegram) bring shoppers' messages into the same inbox; store platforms (Shopify, Salla, Zid, WooCommerce) give the AI your catalogue and live orders; team alerts (Slack, Microsoft Teams, Discord, Telegram) ping you on hand-offs; and contacts and events flow to Klaviyo, Mailchimp, HubSpot and Google Analytics. Aramex, SMSA, Naqel and DHL tell the AI where a parcel is. Resend and SendGrid send email from your own accounts; text messages through Unifonic and Taqnyat come later. Zendesk and Freshdesk get a ticket for each hand-off. With pixel_events on, the chat on your storefront also reports its events to the GA4, Meta and TikTok tags already on your pages.

All integrations are in Beta: built from each provider's documentation and tested with its sample data, not yet with real accounts.

Your account details for each tool are encrypted at rest and never shown back, not even to you: the dashboard shows •••• once a secret is saved.

Headless stores

A store with its own front end and its own cart (Next.js, Hydrogen, Nuxt over Shopify or another platform) has no theme cart for the chat to add to, so your site tells it how. This all runs in the shopper's browser and needs no API key.

1. Load the chat

// app/layout.jsx
import Script from "next/script";
import { MudheefBridge } from "./mudheef-bridge";

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        {children}
        <Script src="https://store-chat-steel.vercel.app/widget.js" data-mudheef-key="wk_..." strategy="afterInteractive" />
        <MudheefBridge />
      </body>
    </html>
  );
}

The chat stays loaded as shoppers move between pages, and reports each new page itself, a second or so after the address changes. It only reads the address: it never wraps or changes your router's history.

2. Let the chat add to your cart

Set window.MudheefStore before or after the script loads (sites that set the older name, window.LabiqStore, keep working). Product cards show Add to cart and the size picker once addToCart exists. Registering it is the store's choice, so this doesn't wait for the selling switch in Settings.

// app/mudheef-bridge.jsx
"use client";
import { useEffect } from "react";
import { useCart } from "./cart"; // your own cart

const id = (gid) => String(gid).split("/").pop(); // gid://shopify/ProductVariant/42 -> "42"

export function MudheefBridge() {
  const cart = useCart();
  useEffect(() => {
    window.MudheefStore = {
      // Required. Resolve when it's in the cart; reject, throw or return false when it isn't.
      addToCart: (item) => cart.addLine(`gid://shopify/ProductVariant/${item.variant_id}`, item.quantity),
      // Optional. The "Go to checkout" link the chat shows after an add.
      checkoutUrl: () => cart.checkoutUrl,
      // Optional. What's in the cart, so the AI and cart campaigns know it. Plain ids, not gid:// ones.
      cart: () => ({ currency: cart.currency, total: cart.total, items: cart.lines.map((l) => (
        { product_id: id(l.merchandise.product.id), variant_id: id(l.merchandise.id), quantity: l.quantity })) }),
      // Optional. Your own product address, for example in the shopper's language.
      productUrl: (item) => `/${document.documentElement.lang}/products/${item.handle}`,
    };
    return () => { delete window.MudheefStore; };
  }, [cart]);
  return null;
}

What addToCart gets:

FieldWhat it is
variant_idThe option to add: the one the shopper picked on the card, the one the AI named, or the only one. Never a guess: with several options and none chosen, Add waits for the shopper. null only for a single option that has no id.
product_id, handleThe product's id and handle in your catalogue (Shopify's plain ids).
title, optionThe product's name and the option's, such as "M" or "Default Title".
price, currencyWhat the card showed. Your cart's own price is the one that counts.
urlThe product's page, on your site when site_url is set (below).
quantityAlways 1.

A refused add tells the shopper it couldn't be added and points them to the product page, as on any store. After every add from the chat, on any store, window also gets a mudheef:add-to-cart event with the same item as its detail, for opening your cart drawer or updating the cart count (the older labiq:add-to-cart still fires too):

window.addEventListener("mudheef:add-to-cart", (e) => openCartDrawer(e.detail));

3. Count the sales the chat helped

window.Mudheef.attribution() (also under the older name window.Labiq) returns { visitor, conversation } for this shopper. Put them on the cart as the attributes _storechat_visitor and _storechat_conversation before checkout. Shopify copies cart attributes into the order, and the order webhook Mudheef gets from your connected Shopify store counts the sale as helped when that shopper had an AI answer in the 7 days before. Attributes starting with _ aren't shown at checkout.

// before sending the shopper to checkout (Shopify Storefront API)
const a = window.Mudheef?.attribution();
if (a) {
  await storefront(`mutation ($cartId: ID!, $attributes: [AttributeInput!]!) {
    cartAttributesUpdate(cartId: $cartId, attributes: $attributes) { userErrors { message } }
  }`, { cartId, attributes: [
    { key: "_storechat_visitor", value: a.visitor },
    { key: "_storechat_conversation", value: a.conversation },
  ] });
}

Other platforms don't carry these attributes into their orders yet. There, an order is matched by the phone or email the shopper left in the chat.

Cards link to the address in your catalogue, which on Shopify is the shop's primary domain. If shoppers buy on another site, set site_url in the settings (PUT /v1/admin/config). Give your site's address and each product keeps its path: "https://www.example.com" links to https://www.example.com/products/<handle>. Or give a pattern with {handle}, such as "https://www.example.com/item/{handle}". A productUrl set in MudheefStore decides last. Campaigns that name the product on the page recognise any page whose address ends in the product's handle, such as /en/store/mm2/p/<handle>.

Your own order-status address

For stores whose orders or deliveries live outside Shopify, Salla, Zid and WooCommerce, such as a delivery bot or your own fulfilment system. Connect Order status under Integrations with an https address and a signing secret, and when a shopper asks about an order, Mudheef asks your system too. In Beta, like every integration.

What Mudheef sends

POST <your https address>
Content-Type: application/json
X-Mudheef-Signature: <hex HMAC-SHA256 of the exact raw body, keyed with your signing secret>

{"order_number":"1001","email":"sara@example.com","sent_at":1762870800}

Check the signature over the raw bytes you received, before parsing the JSON, and compare in constant time:

hmac.compare_digest(hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest(), request.headers["X-Mudheef-Signature"])

Mudheef asks after the store platform's own lookup, and never when the platform already has that order under a different email.

What you answer

When the number and the email both match one order, HTTP 200 with JSON:

{"status":"Delivered","detail":"Sent to Roblox user Sara_123 in Blox Fruits","updated_at":"2026-11-10T14:20:00Z"}
FieldWhat it is
statusRequired. A short phrase, cut to 80 characters.
detailOptional. Cut to 300 characters.
updated_atOptional. ISO 8601.

No such order, or the email doesn't match: HTTP 404, or 200 with {"status": null}. Only your system knows an order's email, so check it yourself; the shopper hears the same thing in both cases. All text is shown on one line, and text written as instructions to the AI isn't used.

Limits: https and public addresses only, 5 seconds for the whole call, one try, at most 16 KB in the answer, redirects not followed. A 401 or 403 shows on the Integrations page as "signature refused"; any other error shows there too, and the shopper hears that the order couldn't be checked. The Test button sends {"order_number":"MUDHEEF-TEST","email":"test@mudheef.invalid",...}: answer 404.