# Ecomas Custom Storefront API & Outbound Webhooks Specification
> Authoritative reference for AI coding agents (Cursor, Claude Code, Windsurf, ChatGPT, Copilot) and developers building headless e-commerce storefronts or bespoke ERPs.

## 1. Architecture Overview
Ecomas connects your custom storefront to conversational AI agents (WhatsApp, Instagram, Facebook Messenger, Web Chat):
- **Inbound (Store -> Ecomas):** Query catalog items, check live stock, search customers, and place orders via REST API.
- **Outbound (Ecomas -> Store Webhook):** When an AI chatbot confirms a customer purchase on social channels, Ecomas sends an HTTP POST event (`order.created`) signed with HMAC-SHA256 directly to your Webhook URL.

---

## 2. Base Configuration & Credentials
Obtain your store credentials from: `https://ecomasai.com/user/integrations/custom`
- **Base URL:** `https://ecomasai.com/api`
- **API Key Format:** `esk_live_...`
- **API Secret Format:** `ess_...`
- **HTTP Headers:**
  ```http
  Authorization: Bearer <YOUR_API_KEY>
  X-Store-Api-Key: <YOUR_API_KEY>
  Content-Type: application/json
  Accept: application/json
  ```

---

## 3. Outbound Webhook: `order.created`
Triggered instantly when an AI agent completes a sale with a customer.

### Delivered Headers
- `Content-Type: application/json`
- `User-Agent: Ecomas-Webhook/1.0`
- `X-Ecomas-Event: order.created`
- `X-Ecomas-Timestamp: <unix_epoch_in_seconds>`
- `X-Ecomas-Signature: <hmac_sha256_hex_string>`
- `X-Ecomas-Shop-Id: <shop_id>`

### Signature Verification Algorithm
```text
signature = HMAC_SHA256(timestamp + "." + raw_json_body, api_secret)
```
1. Reject any request if `abs(now() - timestamp) > 300` (prevents replay attacks).
2. Compute HMAC-SHA256 over `${timestamp}.${raw_json_body}` using your `api_secret`.
3. Use a timing-safe equality check (`crypto.timingSafeEqual` in Node.js or `hash_equals` in PHP) against `X-Ecomas-Signature`.

### Example Webhook JSON Payload
```json
{
  "event": "order.created",
  "timestamp": 1791012345,
  "shop_id": 12,
  "shop_name": "Artisan Craft Studio",
  "data": {
    "order": {
      "id": 1042,
      "status": "pending",
      "total_amount": 149.00,
      "currency": "USD",
      "currency_symbol": "$",
      "notes": "Handle with care",
      "source_channel": "whatsapp",
      "created_at": "2026-10-03T07:30:00+00:00"
    },
    "customer": {
      "id": 589,
      "name": "Sarah Connor",
      "phone": "+14155552671",
      "email": "sarah.connor@example.com",
      "address": "742 Evergreen Terrace, Apt 4B",
      "city": "San Francisco"
    },
    "items": [
      {
        "product_id": 84,
        "title": "Minimalist Ceramic Watch",
        "sku": "WATCH-SL-01",
        "quantity": 1,
        "unit_price": 149.00,
        "total_price": 149.00,
        "variation_id": null
      }
    ]
  }
}
```

### Expected Response
Respond with HTTP status `200 OK` or `201 Created` within 5 seconds:
```json
{
  "received": true,
  "external_order_id": "STORE-ORD-98412"
}
```

---

## 3.1 Outbound Webhooks: `product.updated` & `product.image.updated`
Triggered automatically when product details, pricing, status, or images are modified or generated in Ecomas Studio.

### Example Webhook JSON Payload
```json
{
  "event": "product.image.updated",
  "timestamp": 1791255000,
  "shop_id": 12,
  "shop_name": "Artisan Craft Studio",
  "data": {
    "product_id": 84,
    "external_id": "WATCH-SL-01",
    "sku": "WATCH-SL-01",
    "title": "Minimalist Ceramic Watch",
    "status": "active",
    "price": 149.00,
    "compare_price": null,
    "quantity": 45,
    "action": "replace",
    "image_url": "https://ecomasai.com/storage/product-images/uuid.png",
    "replace_index": 0,
    "images": [
      "https://ecomasai.com/storage/product-images/uuid.png",
      "https://ecomasai.com/storage/product-images/gallery-2.png"
    ],
    "images_detail": [
      {
        "id": 369,
        "url": "https://ecomasai.com/storage/product-images/uuid.png",
        "sort_order": 0,
        "is_primary": true
      }
    ],
    "updated_at": "2026-10-06T03:00:00+00:00"
  }
}
```

---

## 4. Inbound REST API Reference

### 4.1 Search Catalog & List Products
`GET /api/agent/shop/{shop_id}/products`
- **Query Params:** `query` (search keyword), `category`, `limit` (default 20, max 100)
- **Response:**
  ```json
  {
    "products": [
      {
        "id": 84,
        "title": "Minimalist Ceramic Watch",
        "sku": "WATCH-SL-01",
        "price": 149.00,
        "stock": 45,
        "is_active": true,
        "image_url": "https://ecomasai.com/storage/products/watch-1.png",
        "variations": []
      }
    ]
  }
  ```

### 4.2 Check Real-Time Stock
`GET /api/agent/shop/{shop_id}/products/{product_id}/stock`
- **Query Params:** `variation_id` (optional)
- **Response:**
  ```json
  {
    "product_id": 84,
    "in_stock": true,
    "stock_quantity": 45,
    "allow_backorder": false
  }
  ```

### 4.3 Customer Search & Creation
- Search: `GET /api/agent/shop/{shop_id}/customers/search?query=+14155552671`
- Create: `POST /api/agent/shop/{shop_id}/customers`
  ```json
  {
    "name": "Sarah Connor",
    "phone": "+14155552671",
    "email": "sarah.connor@example.com",
    "address": "742 Evergreen Terrace, Apt 4B",
    "city": "San Francisco"
  }
  ```

### 4.4 Ingest Customer Order
`POST /api/agent/shop/{shop_id}/orders`
```json
{
  "customer_id": 589,
  "items": [
    {
      "product_id": 84,
      "quantity": 1,
      "unit_price": 149.00
    }
  ],
  "confirm_order": true,
  "notes": "Gift wrap requested",
  "source_channel": "custom_storefront"
}
```
- **Response:**
  ```json
  {
    "status": "success",
    "order_id": 1042,
    "external_order_id": null,
    "total_amount": 149.00,
    "message": "Order created successfully"
  }
  ```

---

## 5. Webhook Implementation Templates

### Next.js (App Router: `app/api/webhooks/ecomas-order/route.ts`)
```typescript
import { NextRequest, NextResponse } from 'next/server';
import crypto from 'crypto';

const API_SECRET = process.env.ECOMAS_API_SECRET!;

export async function POST(req: NextRequest) {
  const timestamp = req.headers.get('x-ecomas-timestamp');
  const signature = req.headers.get('x-ecomas-signature');

  if (!timestamp || !signature) {
    return NextResponse.json({ error: 'Missing signature headers' }, { status: 401 });
  }

  // Reject timestamps older than 5 minutes
  if (Math.abs(Math.floor(Date.now() / 1000) - parseInt(timestamp, 10)) > 300) {
    return NextResponse.json({ error: 'Timestamp expired' }, { status: 401 });
  }

  const rawBody = await req.text();
  const expectedSignature = crypto
    .createHmac('sha256', API_SECRET)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex');

  if (!crypto.timingSafeEqual(Buffer.from(signature, 'hex'), Buffer.from(expectedSignature, 'hex'))) {
    return NextResponse.json({ error: 'Invalid HMAC signature' }, { status: 403 });
  }

  const payload = JSON.parse(rawBody);
  const order = payload.data.order;

  // Save order to your database or dispatch fulfillment...

  return NextResponse.json({ received: true, external_order_id: `NEXT-${order.id}` });
}
```

### Node.js / Express
```javascript
const express = require('express');
const crypto = require('crypto');
const app = express();

app.post('/api/webhooks/ecomas-order', express.raw({ type: 'application/json' }), (req, res) => {
  const timestamp = req.headers['x-ecomas-timestamp'];
  const signature = req.headers['x-ecomas-signature'];
  const apiSecret = process.env.ECOMAS_API_SECRET;

  if (!timestamp || !signature) return res.status(401).json({ error: 'Missing headers' });
  if (Math.abs(Math.floor(Date.now() / 1000) - parseInt(timestamp, 10)) > 300) {
    return res.status(401).json({ error: 'Expired timestamp' });
  }

  const rawBody = req.body.toString('utf8');
  const expected = crypto.createHmac('sha256', apiSecret).update(`${timestamp}.${rawBody}`).digest('hex');
  if (!crypto.timingSafeEqual(Buffer.from(signature, 'hex'), Buffer.from(expected, 'hex'))) {
    return res.status(403).json({ error: 'Invalid signature' });
  }

  const payload = JSON.parse(rawBody);
  return res.status(200).json({ received: true, external_order_id: `EXPRESS-${payload.data.order.id}` });
});
```

### Python (FastAPI)
```python
from fastapi import FastAPI, Request, HTTPException, Header
import hmac, hashlib, time, os, json

app = FastAPI()
API_SECRET = os.getenv("ECOMAS_API_SECRET", "ess_...")

@app.post("/api/webhooks/ecomas-order")
async def handle_ecomas_webhook(request: Request, x_ecomas_timestamp: str = Header(None), x_ecomas_signature: str = Header(None)):
    if not x_ecomas_timestamp or not x_ecomas_signature:
        raise HTTPException(status_code=401, detail="Missing headers")
    if abs(time.time() - int(x_ecomas_timestamp)) > 300:
        raise HTTPException(status_code=401, detail="Expired timestamp")

    raw_body = await request.body()
    signed = f"{x_ecomas_timestamp}.{raw_body.decode('utf-8')}".encode('utf-8')
    expected = hmac.new(API_SECRET.encode('utf-8'), signed, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, x_ecomas_signature):
        raise HTTPException(status_code=403, detail="Invalid signature")

    payload = json.loads(raw_body)
    return {"received": True, "external_order_id": f"FASTAPI-{payload['data']['order']['id']}"}
```

### PHP / Laravel
```php
namespace App\Http\Controllers;

use Illuminate\Http\Request;

class EcomasWebhookController extends Controller
{
    public function handle(Request $request)
    {
        $timestamp = $request->header('X-Ecomas-Timestamp');
        $signature = $request->header('X-Ecomas-Signature');
        $secret = config('services.ecomas.api_secret');

        if (! $timestamp || ! $signature || abs(now()->timestamp - (int) $timestamp) > 300) {
            return response()->json(['error' => 'Invalid or expired signature'], 401);
        }

        $expected = hash_hmac('sha256', "{$timestamp}.{$request->getContent()}", $secret);
        if (! hash_equals($expected, $signature)) {
            return response()->json(['error' => 'Invalid signature'], 403);
        }

        $order = $request->input('data.order');
        return response()->json(['received' => true, 'external_order_id' => 'LARAVEL-' . $order['id']]);
    }
}
```