Skip to content
FloeeAPI

API reference · Guides

Webhooks

We POST the events you choose (new messages, and orders being placed, paid, cancelled or refunded) to a URL you choose, signed with a secret only you and we know. Use it to mirror conversations into a CRM, push orders into your store or kitchen, or keep an audit copy.

Set it up

  1. In the dashboard, open Integrations, Webhooks, enter an HTTPS URL and tick the events you want. One endpoint per workspace.
  2. Copy the signing secret. It is shown once, when the webhook is first saved or when you rotate it.
  3. Answer every delivery with a 2xx status within 10 seconds.

URLs that resolve to private or internal addresses are refused, and a redirect is treated as a failed delivery rather than followed.

Events

EventWhen
message.createdA message was added to any conversation, on any channel: the customer's, the agent's reply and a team member's reply. Internal notes are never sent. The default subscription.
order.placedAn order was placed by the agent, on a call or in chat. Stock for it is held from this moment.
order.paidThe customer paid the order's payment link, or your store reported it paid.
order.cancelledThe order was cancelled. order.expired is true when it was an unpaid order whose payment window closed (30 minutes by default) and its stock was put back on sale.
order.refundedThe order was refunded.
message.created bodyjson
{
  "event": "message.created",
  "message": {
    "id": "f2a3b4c5-d6e7-4f8a-9b0c-1d2e3f4a5b6c",
    "conversation_id": "6f1c2a90-3b7e-4d55-9a1e-2c8f0b7d4e11",
    "role": "assistant",
    "content": "Ada! Nasi lemak ayam goreng RM12.90. Nak order berapa?",
    "channel": "whatsapp",
    "created_at": "2026-09-27T03:14:07.512Z"
  }
}

Read the full conversation with GET /api/v1/conversations/{id}/messages.

order.placed bodyjson
{
  "event": "order.placed",
  "order": {
    "id": "0b8f3c2e-7a41-4d9e-b6c5-3e2f1a0d9c87",
    "bot_id": "9d7e6f5a-4b3c-4d2e-8f1a-0b9c8d7e6f5a",
    "order_number": "ORD-20260927-4K7QZ",
    "status": "pending_payment",
    "channel": "whatsapp",
    "items": [
      {
        "name": "Nasi Lemak (Ayam)",
        "quantity": 2,
        "unitPriceSen": 1290,
        "specialInstructions": "no sambal",
        "productId": "5c4b3a29-1807-4f6e-9d5c-4b3a29180716",
        "variantId": "7e6d5c4b-3a29-4180-8f6e-5d4c3b2a1908"
      }
    ],
    "subtotal": 25.8,
    "total": 25.8,
    "currency": "MYR",
    "conversation_id": "6f1c2a90-3b7e-4d55-9a1e-2c8f0b7d4e11",
    "customer": {
      "name": "Siti",
      "phone": "+60123456789"
    },
    "payment_url": null,
    "payment_expires_at": "2026-09-27T03:44:07.512Z",
    "expired": false,
    "paid_at": null,
    "external_id": null,
    "created_at": "2026-09-27T03:14:07.512Z"
  }
}

order.paid, order.cancelled and order.refunded carry the same order object as it stands after the change. Acknowledge an order in your store with POST /api/v1/orders/{id}/status.

Headers

HeaderValue
Content-Typeapplication/json
X-IceBot-Signaturesha256= followed by the hex HMAC-SHA256 of the raw request body, keyed with your signing secret.
X-IceBot-EventThe event name, e.g. message.created or order.placed.
X-IceBot-DeliveryThe delivery's id. The same on every retry of it, so you can de-duplicate on it.

Verify the signature

Compute the HMAC over the exact bytes you received and compare in constant time. Refuse anything that does not match.

Node.js (Express)javascript
import crypto from 'node:crypto';
import express from 'express';

const app = express();

// Verify against the RAW body: re-serialising parsed JSON changes the bytes.
app.post('/icebot', express.raw({ type: 'application/json' }), (req, res) => {
  const expected =
    'sha256=' + crypto.createHmac('sha256', process.env.ICEBOT_WEBHOOK_SECRET).update(req.body).digest('hex');
  const got = req.get('X-IceBot-Signature') ?? '';
  const ok =
    got.length === expected.length && crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected));
  if (!ok) return res.status(401).end();

  const event = JSON.parse(req.body.toString('utf8'));
  // Deliveries can repeat after a timeout: de-duplicate on the delivery id.
  // req.get('X-IceBot-Delivery')
  res.status(200).end(); // answer fast, then do the work
  handle(event);
});

Delivery and retries

  • Deliveries are sent by a worker that runs every 5 minutes, so expect up to that delay. Webhooks are for syncing, not for replying in real time.
  • Any non-2xx status, a redirect, or no answer within 10 seconds counts as a failure. We retry after 1 min, 5 min, 30 min, 2 h, 6 h.
  • After 6 failed attempts the delivery is marked failed and not retried. Use the conversations API to backfill anything you missed.
  • Order is not guaranteed and a delivery can arrive more than once. De-duplicate on the X-IceBot-Delivery header.