Shopletzy

Set up order webhooks

Connect your store to external systems with order events, signed requests, and reliable webhook handling.


Order webhooks send a JSON request to your server when a selected event occurs in your Shopletzy store. Use them to update an external system or build a supplier integration.

Webhooks are included in all Shopletzy plans, including Discover (free). Configuration is available only through the Shopletzy web portal, not the mobile app.

A webhook delivers an event; it does not automatically place an order in a supplier’s system. Your receiver must translate the event into the supplier’s required workflow.

1. Prepare your endpoint

Create a publicly reachable HTTPS endpoint that accepts JSON POST requests. Keep it on a server you control, and prepare a durable inbox or queue for accepted events.

Choose a long, random signing secret, for example with Node.js:

node -e "console.log(require('node:crypto').randomBytes(32).toString('hex'))"

Save the secret in your server’s secret manager or environment configuration. Use the same exact string in Shopletzy. Do not put it in browser code, a public repository, or a webhook URL.

2. Configure your store

  1. Sign in to the Shopletzy web portal and open your store.
  2. Go to Settings → Developer → Webhook.
  3. Select Add Webhook.
  4. Enter your Webhook URL and Secret.
  5. Select the events you want to receive, then select Add to save.
  6. When editing an existing webhook, check Activation Status and select Update to save your changes.

New webhook subscriptions are active when created. Set up and verify your receiver before adding the subscription. The secret authenticates deliveries through a request signature; there is no separate verification challenge to answer.

For a first test, select Order created and place a new COD order. Selecting Order shipped alone will only send an event when the order is shipped. Events are opt-in: updating your subscription does not select new event types automatically or replay past orders.

3. Choose the right events

EventMeaning
order.createdThe order reached awaitingFulfilment. Includes COD orders; this event alone is not proof of payment.
order.paidThe recorded payment is paid: an online order reached awaitingFulfilment, or a COD order reached completed with payment recorded as paid.
order.shippedThe order was marked shipped.
order.ready_for_pickupA pickup order is ready for the customer to collect.
order.completedThe order was marked completed.
order.cancelledThe order was marked cancelled.
order.declinedThe merchant declined the order.
order.returnedThe order was marked returned.
order.refundedThe incoming order status is refunded.
order.partially_refundedThe incoming order status is partiallyRefunded.
order.payment_failedThe payment flow published a payment failure.
order.awaiting_confirmationThe order is waiting for confirmation, such as after a payment-proof upload.
order.pendingThe incoming order status is pending.
order.awaiting_paymentThe incoming order status is awaitingPayment.
order.expiredThe incoming order status is expired.

The catalogue includes mappings for these statuses. Delivery depends on the owning order or payment workflow publishing the event. In particular, do not rely on every pending, awaiting-payment, expiry, or refund path emitting a notification. A cancellation event does not by itself prove that a refund succeeded. Use reconciliation for workflows that require a complete record.

For forward only after payment, subscribe to order.paid. Do not treat a new COD order, a payment-proof upload, or a fulfilment event as payment confirmation. COD completion follows Shopletzy’s recorded collection state; it does not confirm a courier’s remittance to your bank account. Webhooks report recorded state, rather than querying the payment provider independently.

4. Read the request

Shopletzy sends:

POST /webhooks/shopletzy HTTP/1.1
Content-Type: application/json
X-Shopletzy-Signature: <64-character hexadecimal HMAC-SHA256 digest>

Example order.paid body, with illustrative identifiers and a shortened order payload:

{
  "id": "b6b711ba-6f52-5e1f-9382-4b4cc57bd17e",
  "event": "order.paid",
  "payload": {
    "id": "8ce10241-473e-466a-ab3e-262d28ba3246",
    "sid": "71f0a13f-d9f1-4a0f-b4b3-20889ac4a482",
    "orderNo": "example-order-1001",
    "status": "awaitingFulfilment",
    "paymentStatus": "paid",
    "payment": { "mode": "online" },
    "pickup": false,
    "items": [
      {
        "id": "example-item",
        "title": "Cotton T-shirt",
        "sku": "TEE-BLUE-M",
        "quantity": 1,
        "price": 499
      }
    ],
    "shippingAddress": {
      "fullName": "Example Customer",
      "addressLine": "Example Street",
      "city": "Bengaluru",
      "state": "Karnataka",
      "country": "India",
      "zipcode": "560001"
    },
    "billing": { "itemSubtotal": 499, "toPay": 499 },
    "createdAt": "2026-10-21T10:00:00Z"
  },
  "createdAt": "2026-10-21T10:00:00Z"
}
  • The outer id identifies the webhook event. Use it for duplicate handling. It differs from the order ID at payload.id.
  • payload.sid identifies the store. Check it against the stores your receiver serves.
  • The payload also carries customer details, items, billing information, addresses and shipping information where present. Some fields can be absent or null. Do not log the full payload by default.
  • The outer createdAt currently reflects the order’s creation time, not the delivery time or the time of a later status change. Do not use it as a freshness check for a shipped or paid notification.
  • Treat payloads as extensible: tolerate additional fields. Do not assume that payment-provider credentials or gateway response details are included.

5. Verify the signature

The signature is the lowercase hexadecimal HMAC-SHA256 of the exact request body bytes, using the configured secret string as the key. There is no sha256= prefix. If your chosen secret happens to contain hexadecimal characters, use it as a string; do not decode it into bytes first.

Verify before accepting or acting on an event. Parsing JSON and serializing it again can change whitespace or field order and invalidate the signature. There is no bearer token or separate API key in these deliveries.

This Node.js/Express example exports a router. Supply an acceptOnce function that atomically stores the event in a durable inbox with a unique event ID, returning successfully for an existing ID. A worker can process the stored event later. The signing secret must be present when the server starts.

// shopletzy-webhooks.cjs
const express = require("express");
const { createHmac, timingSafeEqual } = require("node:crypto");

function createShopletzyWebhookRouter({ secret, allowedStoreIds, acceptOnce }) {
  if (!secret || !allowedStoreIds?.length || typeof acceptOnce !== "function") {
    throw new Error("Signing secret, allowed stores and durable inbox are required");
  }

  const router = express.Router();
  const stores = new Set(allowedStoreIds);

  router.post("/", express.raw({ type: "application/json", limit: "1mb" }),
    async (req, res) => {
      const signature = req.get("X-Shopletzy-Signature") || "";
      if (!Buffer.isBuffer(req.body) || !/^[0-9a-f]{64}$/i.test(signature)) {
        return res.sendStatus(401);
      }

      const expected = createHmac("sha256", secret).update(req.body).digest();
      const received = Buffer.from(signature, "hex");
      if (!timingSafeEqual(expected, received)) return res.sendStatus(401);

      let event;
      try {
        event = JSON.parse(req.body.toString("utf8"));
      } catch {
        return res.sendStatus(400);
      }

      if (!event || typeof event.id !== "string" || !event.id ||
          typeof event.event !== "string" || !event.event.startsWith("order.") ||
          !event.payload || typeof event.payload.id !== "string" ||
          !stores.has(event.payload.sid)) {
        return res.sendStatus(400);
      }

      try {
        await acceptOnce(event); // Commit durable storage before acknowledging.
        return res.sendStatus(204);
      } catch {
        return res.sendStatus(503); // Do not acknowledge an event you could lose.
      }
    });

  return router;
}

module.exports = { createShopletzyWebhookRouter };

The example limits request bodies to 1 MB. Choose a suitable limit for your largest orders.

Mount this router before your application’s JSON parser:

const express = require("express");
const { createShopletzyWebhookRouter } = require("./shopletzy-webhooks.cjs");
const { acceptOnce } = require("./webhook-inbox.cjs"); // Your durable inbox adapter.
const app = express();

app.use("/webhooks/shopletzy", createShopletzyWebhookRouter({
  secret: process.env.SHOPLETZY_WEBHOOK_SECRET,
  allowedStoreIds: [process.env.SHOPLETZY_STORE_ID].filter(Boolean),
  acceptOnce,
}));
app.use(express.json()); // Other application routes can use parsed JSON.
app.listen(3000);

The inbox adapter is application-specific and is not included above. Use a unique constraint on the event ID, an atomic insert-or-ignore, and a worker that retries processing. For shared receivers across tenants, scope inbox entries and access to the verified store. Avoid a separate “check then insert”: concurrent deliveries can both pass that check.

The signature authenticates the body but does not contain a signed delivery timestamp. Keep durable duplicate records for as long as your business needs to prevent replayed side effects. Use the event ID as an idempotency key when the downstream supplier supports one.

If your host or framework parses the request first, configure its original raw-body support. Do not compute the HMAC over JSON.stringify(req.body).

Delivery, retries and changes

  • Return 2xx after signature validation and durable acceptance, including for an already accepted duplicate. Respond directly rather than relying on redirects; non-2xx final responses are failures.
  • Deliveries are asynchronous. Allow for queueing and receiver startup; there is no guaranteed fixed delivery time.
  • Retries can produce duplicates and events can arrive out of order. Use event IDs and business-state checks before issuing supplier orders or moving an order backwards in its lifecycle.
  • Paid event IDs are stable per store/order. Other event IDs are stable when the same source event is retried, but a separately published notification can have a new ID. Protect business actions with an order-level key as well when appropriate.
  • Delivery is best effort, not a complete audit log or guaranteed replay service. A subscription edited or deleted after a delivery was queued can cause that queued delivery to be skipped.
  • Repeated delivery failures can disable a subscription after at least five consecutive failures spanning more than 24 hours. Check its activation status after fixing your receiver. Saving an update clears the failure history.
  • Changing the secret requires coordinating the receiver and dashboard. There is no automatic overlapping-secret rotation window. Previously queued requests can carry the old signature; changing the subscription can also skip queued deliveries.

Troubleshooting

SymptomWhat to check
No request after placing a COD orderSelect Order created, confirm the webhook is active, save, and place a new order. Order paid is not expected at COD placement.
No request after shippingSelect Order shipped. Check endpoint reachability, activation status and receiver logs. Allow for asynchronous delivery.
Signature rejectedUse the same secret string, the original raw body, hexadecimal SHA256 output and the X-Shopletzy-Signature header. Check whether a JSON parser ran first.
Duplicate supplier orderDeduplicate both accepted events and the downstream action. Retries and separate order events can refer to the same order.
Event missing after changing settingsPreviously queued deliveries may be skipped. A saved subscription does not replay historical orders.
Webhook becomes inactiveFix endpoint failures, check activation status and save the configuration again.

To ask support to trace a delivery, share your store name, order number, selected event, approximate action time with timezone, and the HTTP status seen by your receiver. Do not send your signing secret, full customer payload, or a private endpoint URL containing credentials.