Webhooks and integrations API (Custom Store)
Push products, customers and orders from your own website into EcomUp with an API key, and receive signed order webhooks.
Connect your own website to EcomUp with the Custom Store integration. Your site sends products, categories, customers, addresses and orders into EcomUp through a simple API. EcomUp sends events back to your site through a webhook (an automatic message to a URL you choose) when orders are created or change status. This guide is written for the developer who builds your website.
Before you start
- Your role needs permission to manage connections (owners and admins have it by default).
- Your website needs a server that can send HTTPS requests and, for webhooks, a public HTTPS URL that can receive them.
- Read Developer overview if you're not sure Custom Store is the right fit. WooCommerce stores have their own no-code connection.
Steps
- In the left menu, open Connections, find Custom Store, and click Connect.
- Click Connect Custom Store.
- Copy the API key shown under API key — send as x-api-key header. It's shown only once.

Copy your API key (1) right away (Regenerate (2) makes a new one); the endpoints are listed below (3). - Copy the endpoint URLs from the Inbound — API list. Each one has a copy button in the documentation panel.
- Under Outbound — Order Webhook, type your URL in Your API endpoint (for example https://yourstore.com/api/webhooks/orders) and click Save Webhook.
- Click the eye icon next to Signing secret and copy it. Your server uses it to check that webhooks really come from EcomUp.

Type Your API endpoint (1), click Save Webhook (2), and copy the Signing secret with the eye icon (3). - Send a test product from your server, then check it appears in Products.
Tips
- Push in this order: categories and products first, then customers and addresses, then orders. Order line items link to products by
externalProductId. externalIdis your own ID for each record. EcomUp uses it to avoid duplicates, so keep it stable.- Always send
"currency": "BDT"(or your store currency) with orders. If you leave it out, the order is saved in USD. - Send prices as strings for products (
"49.99") and as numbers for order line items (49.99). - Unknown fields are rejected. Only send the fields listed below.
- Webhook deliveries are not retried, and EcomUp waits up to 10 seconds for your reply. Return a 2xx response quickly and do slow work afterwards.
- Click Regenerate if your key leaks. The old key stops working immediately.
Troubleshooting
“Missing x-api-key header” or “Invalid API key” (401)
Send the key in a header named exactly x-api-key. If you regenerated the key or disconnected the Custom Store, update your server with the new key.
“price must be a non-negative number (e.g. 10 or 10.99)” (400)
Product prices must be strings of digits with an optional decimal point, with no currency symbol or commas.
“property … should not exist” (400)
You sent a field EcomUp doesn't accept. Remove it and try again.
“customerExternalId not found — create the customer first via POST /webhook/custom-store/customers” (400)
Create the customer before you add an address for them.
“Provide either customer or customerId” (400)
Every order needs a customer. Send an inline customer object, or the customerId returned by the customers endpoint.
Order items show without a product link
A line item's externalProductId didn't match any product you pushed. Push that product first, using the same ID as its externalId.
I'm not receiving webhooks
Check the URL is public, uses HTTPS, and replies within 10 seconds. Note that order.created is sent for orders created in the EcomUp dashboard or by the AI agent, not for orders you pushed in yourself (to avoid echoing your own data back).
“Invalid signature” on my server
Compute the HMAC over the raw request body exactly as received, before any JSON parsing, and use the current signing secret.
For developers
Authentication and base URL
All inbound requests are POST with a JSON body and two headers. Copy the full URL for each endpoint from the Custom Store page; it looks like https://api.ecomup.ai/webhook/custom-store/products.
POST /webhook/custom-store/{resource}
Content-Type: application/json
x-api-key: YOUR_API_KEY
Endpoints
| Endpoint | What it does | Response |
|---|---|---|
/webhook/custom-store/products | Create or update a product (matched by externalId) | { "success": true, "product": { … } } |
/webhook/custom-store/categories | Find or create a category by name | { "success": true, "category": { "id", "name", "slug" } } |
/webhook/custom-store/customers | Find or create a customer | { "success": true, "customerId": "…" } |
/webhook/custom-store/addresses | Add a shipping address to a customer | { "success": true, "addressId": "…" } |
/webhook/custom-store/orders | Create an order with line items, customer and address | { "success": true } |
Products
| Field | Type | Required | Notes |
|---|---|---|---|
externalId | string | Yes | Your product ID (dedupe key), max 255 |
name | string | Yes | Max 255 |
price | string (decimal) | Yes | Current selling price, e.g. "1250" or "49.99" |
sku | string | No | |
regularPrice | string (decimal) | No | Defaults to price |
salePrice | string (decimal) | No | |
description, shortDescription | string | No | |
imageUrl | string (URL) | No | Main image |
images | string[] (URLs) | No | Gallery |
stockQuantity | number | No | |
stockStatus | enum | No | instock, outofstock, onbackorder |
status | enum | No | draft, publish, pending |
category | string | No | Category name; created if it doesn't exist |
curl -X POST https://api.ecomup.ai/webhook/custom-store/products \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"externalId": "sku-123",
"name": "Cotton Panjabi",
"price": "1250",
"regularPrice": "1500",
"category": "Panjabi",
"imageUrl": "https://cdn.example.com/panjabi.jpg",
"stockQuantity": 20
}'
Categories
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | Yes | Found or created; safe to send again |
Customers
| Field | Type | Required | Notes |
|---|---|---|---|
externalId | string | Yes | Your customer ID (dedupe key) |
firstName | string | Yes | |
lastName | string | No | |
phone | string | No |
Addresses
| Field | Type | Required | Notes |
|---|---|---|---|
customerExternalId | string | Yes | The customer must already exist |
addressLine1 | string | Yes | Max 255 |
city | string | Yes | Max 60 |
addressLine2 | string | No | Max 255 |
state | string | No | Max 60 |
postalCode | string | No | Max 20 |
country | string | No | Max 40 |
phone | string | No | Max 20 |
Orders
| Field | Type | Required | Notes |
|---|---|---|---|
externalId | string | Yes | Your order ID (dedupe key) |
status | enum | No | pending (default), processing, on-hold, completed, cancelled, refunded, failed, paid |
paymentMethod | string | No | e.g. cod |
paymentStatus | string | No | Defaults to pending |
currency | string | No | 3-letter code. Defaults to USD, so send BDT |
customerId | uuid | One of these | ID returned by /customers |
customer | object | One of these | externalId, firstName required; lastName, phone optional |
shippingAddressId | uuid | No | ID returned by /addresses; wins over address |
address | object | No | Same fields as the Addresses endpoint, without customerExternalId |
lineItems | array (min 1) | Yes | Each: externalProductId (string), quantity (number, ≥ 1), price (number, unit price ≥ 0), optional productName |
customerNote | string | No |
{
"externalId": "order-001",
"status": "pending",
"currency": "BDT",
"paymentMethod": "cod",
"customer": { "externalId": "cust-123", "firstName": "Rahim", "phone": "01712345678" },
"address": { "addressLine1": "House 12, Road 5, Dhanmondi", "city": "Dhaka" },
"lineItems": [{ "externalProductId": "sku-123", "quantity": 2, "price": 1250 }]
}
Outbound webhook events
EcomUp sends a POST with Content-Type: application/json to the URL you saved.
| Event | Sent when | Data key |
|---|---|---|
order.created | An order is created in the EcomUp dashboard (including duplicated orders) or by the AI sales agent | order |
order.updated | An order's status changes in EcomUp | order |
product.created | A product is created in EcomUp (dashboard or AI) | product |
customer.created | A customer is created in EcomUp | customer |
category.created | A category is created in EcomUp | category |
Data you push in through the inbound API is not sent back to you. Order events include the full order with its line items, customer and shipping address.
POST https://yourstore.com/api/webhooks/orders
Content-Type: application/json
X-EcomUp-Signature: sha256=5f2b…
{
"event": "order.updated",
"order": {
"id": "…",
"orderNumber": 1042,
"status": "paid",
"amount": "2500.00",
"currency": "BDT",
"paymentMethod": "cod",
"paymentStatus": "paid",
"lineItems": [ { "productId": "…", "productName": "Cotton Panjabi", "quantity": 2, "price": 1250, "total": 2500 } ],
"customer": { "id": "…", "firstName": "Rahim", "phone": "01712345678" },
"shippingAddress": { "id": "…", "addressLine1": "House 12, Road 5, Dhanmondi", "city": "Dhaka" }
},
"timestamp": "2026-10-30T12:34:56.789Z"
}
Verify the signature
X-EcomUp-Signature is sha256= followed by the hex HMAC-SHA256 of the raw request body, using your signing secret as the key. Reject requests where it doesn't match.
// Node.js (Express): use express.raw() so you get the raw body
const crypto = require("crypto");
const expected = "sha256=" +
crypto.createHmac("sha256", SIGNING_SECRET).update(rawBody).digest("hex");
const received = req.header("X-EcomUp-Signature") || "";
const ok = expected.length === received.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received));
if (!ok) return res.status(401).send("Invalid signature");
res.sendStatus(200);
<?php
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_ECOMUP_SIGNATURE'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', $payload, $signingSecret);
if (!hash_equals($expected, $signature)) {
http_response_code(401);
exit('Invalid signature');
}
http_response_code(200);