Custom CMS guide
Connect a shop built on your own platform, from key to go-live, with a complete PHP example.
This guide builds what the WooCommerce plugin does, on any platform. You need a server-side language that can make HTTPS requests and one public route. The examples use plain PHP; the same steps work in Laravel, Django, Node.js or Go.
What you build
Store the key
Create a connection key in the assistant's فروشگاه section and keep it in your server's environment, for example
MANOVA_STORE_KEY. Never put it in templates or front-end code.
Connect once
Call POST /store/connect from an admin action or a deploy script. Save the
publicKey it returns.
Embed the widget
Print the widget tag in your layout with that key:
<script src="https://manova.ir/widget.js" data-bot="<?= htmlspecialchars($publicKey) ?>" async></script>Sync products
A full sync from a scheduled job (nightly is plenty), plus a single-product update from your product save and delete hooks. See Product sync.
Answer order lookups
Add POST {apiUrl}/order. See Order lookup.
A small client
<?php
final class ManovaStore
{
private const BASE = 'https://manova.ir/api/v1/external/store';
public function __construct(private string $key) {}
public function connect(string $siteUrl, string $apiUrl): array
{
return $this->call('POST', '/connect', ['siteUrl' => $siteUrl, 'apiUrl' => $apiUrl,
'currency' => 'IRT', 'pluginVersion' => 'my-cms-1.0']);
}
/** Sends every product, 100 at a time, then removes what was not sent. */
public function fullSync(iterable $products): array
{
$syncId = 'sync_' . bin2hex(random_bytes(8));
$this->call('POST', '/sync', ['syncId' => $syncId, 'phase' => 'start']);
$batch = [];
foreach ($products as $product) {
$batch[] = $product;
if (count($batch) === 100) { $this->call('POST', '/products', ['syncId' => $syncId, 'products' => $batch]); $batch = []; }
}
if ($batch) $this->call('POST', '/products', ['syncId' => $syncId, 'products' => $batch]);
return $this->call('POST', '/sync', ['syncId' => $syncId, 'phase' => 'finish']);
}
/** One changed product, or ids of deleted ones. */
public function update(array $products, array $deleted = []): array
{
return $this->call('POST', '/products', ['products' => $products, 'deleted' => $deleted]);
}
private function call(string $method, string $path, ?array $body = null): array
{
$ch = curl_init(self::BASE . $path);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $this->key, 'Accept: application/json', 'Content-Type: application/json'],
CURLOPT_POSTFIELDS => $body === null ? null : json_encode($body, JSON_UNESCAPED_UNICODE),
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$data = json_decode((string) $response, true) ?? [];
if ($status === 429) throw new RuntimeException('Rate limited, retry later');
if ($status >= 400) throw new RuntimeException(($data['code'] ?? 'HTTP_' . $status) . ': ' . ($data['message'] ?? ''));
return $data;
}
}Map your product rows to Manova's fields:
function toManova(array $row): array
{
$onSale = $row['sale_price'] && $row['sale_price'] < $row['price'];
return [
'id' => (int) $row['id'],
'name' => $row['title'],
'url' => 'https://shop.example.com/p/' . $row['slug'],
'image' => $row['image_url'],
'sku' => $row['sku'],
'price' => $onSale ? $row['sale_price'] : $row['price'],
'price_text' => number_format($onSale ? $row['sale_price'] : $row['price']) . ' تومان',
'regular_price_text' => $onSale ? number_format($row['price']) . ' تومان' : null,
'in_stock' => $row['stock'] > 0,
'stock_quantity' => (int) $row['stock'],
'categories' => $row['category_names'],
'summary' => $row['description_html'],
'cart_url' => 'https://shop.example.com/cart/add/' . $row['id'],
'total_sales' => (int) $row['sold'],
'visible' => $row['status'] === 'published',
];
}Test the order route locally
Sign a request yourself to see that your route accepts it:
SECRET="${MANOVA_STORE_KEY##*_}"
BODY='{"number":"10457","phone":"09121234567"}'
TS=$(date +%s)
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')
curl -i -X POST https://shop.example.com/api/manova/order \
-H "Content-Type: application/json" \
-H "X-Manova-Timestamp: $TS" \
-H "X-Manova-Signature: $SIG" \
-d "$BODY"Also check the failures: a changed body or a timestamp older than five minutes must get 401, and a wrong phone
must get the same 404 {"found": false} as an unknown order.
Go-live checklist
- The key is only in server configuration.
-
connectreturned"active": true, and the widget appears on the shop. - A full sync finished, and the panel's فروشگاه section shows the right product count.
- Saving a product in your admin updates it in the panel within a minute.
- The order route verifies the signature and the timestamp, and answers in well under 10 seconds.
- In the widget on your shop, "پیگیری سفارش 10457 09121234567" returns the real order.