اربط متجرك بأنظمة المحاسبة والمستودعات وتطبيقاتك الخاصة: اقرأ الطلبات وأنشئها، زامن المنتجات والمخزون والعملاء، واستقبل الأحداث لحظياً عبر الويب هوك.
واجهة REST بصيغة JSON. كل طلب يحمل رمزاً خاصاً بمتجر واحد، والمتجر يُحدَّد من الرمز وحده، فلا يمكن لرمز أن يصل إلى بيانات متجر آخر. الحقول بصيغة camelCase، والتواريخ ISO 8601 بتوقيت UTC، والمبالغ أرقام بعملة الطلب أو المتجر مع حقل currency.
من لوحة التحكم: الإعدادات > رموز الربط ثم رمز جديد. اختر الصلاحيات التي يحتاجها نظامك فقط، وانسخ الرمز فوراً لأنه يظهر مرة واحدة. أرسله في كل طلب:
Authorization: Bearer shf_xxxxxxxxxxxxxxxxxxxxxxxx| الصلاحية | تسمح بـ |
|---|---|
catalog:read | عرض المنتجات والتصنيفات والعلامات التجارية والمخزون والأسعار. |
catalog:write | إنشاء وتعديل المنتجات والتصنيفات والأسعار والمخزون. |
orders:read | عرض الطلبات وحالاتها وبيانات العملاء المرتبطة بها. |
orders:write | تغيير حالة الطلبات وتحديث بياناتها. لا يشمل الاسترداد المالي. |
customers:read | عرض العملاء وبيانات التواصل والعناوين وسجل الطلبات. |
customers:write | إنشاء وتعديل العملاء وبياناتهم. |
صلاحية الكتابة تشمل القراءة على نفس القسم. الرمز بلا صلاحيات لا يستطيع شيئاً، والرمز المُلغى أو المنتهي يُرفض فوراً بـ 401. الطلب بصلاحية ناقصة يُرفض بـ 403 مع details.requiredScope.
https://shofniplus.com/api/v1تعمل الواجهة أيضاً على نطاق متجرك الخاص تحت /api/v1. مثال سريع:
curl "https://shofniplus.com/api/v1/orders?limit=5" \
-H "Authorization: Bearer $SHOFNI_TOKEN"120 طلباً في الدقيقة لكل رمز (نافذة متحركة). كل استجابة تحمل X-RateLimit-Limit و X-RateLimit-Remaining و X-RateLimit-Reset. عند التجاوز تصلك 429 مع Retry-After بالثواني. حجم جسم الطلب حتى 4 ميغابايت (وإلا 413).
HTTP/1.1 429 Too Many Requests
Retry-After: 12
{ "error": "rate_limited", "retryAfter": 12 }{
"error": {
"code": "validation_failed",
"message": "\"name\" is required.",
"details": { "field": "name" }
}
}| HTTP | code | المعنى |
|---|---|---|
| 401 | unauthorized | رمز مفقود أو غير صالح أو ملغى. |
| 403 | forbidden | الرمز لا يملك الصلاحية المطلوبة. |
| 404 | not_found | غير موجود في هذا المتجر. |
| 409 | conflict | تعارض، مثل SKU مكرر. |
| 422 | validation_failed | بيانات غير صالحة؛ details.field يحدد الحقل. |
| 429 | rate_limited | تجاوزت حد الاستخدام. |
| 500 | internal_error | خطأ لدينا؛ أعد المحاولة لاحقاً. |
اعتمد على code في برنامجك، فالرسالة نصية للبشر وقد تتغير.
القوائم تقبل ?page= و ?limit= (الحد الأقصى 100، الافتراضي 25) وترتّب الأحدث أولاً، وتعيد:
{
"data": [ ... ],
"pagination": { "page": 1, "limit": 25, "total": 240, "totalPages": 10 }
}المرشحات الشائعة حيث تنطبق: created_after و created_before و updated_after (تواريخ ISO 8601) و q للبحث. للمزامنة التدريجية استخدم updated_after مع آخر وقت مزامنة. المرشحات الخاصة بكل مسار مذكورة في المرجع أدناه.
POST /orders يقبل ترويسة Idempotency-Key (حتى 200 حرف). تكرار نفس المفتاح على نفس المتجر خلال 24 ساعة يعيد الطلب الأول بدل إنشاء طلب ثانٍ، بالحالة 200 مع Idempotent-Replayed: true (الإنشاء الأول 201). بقية مسارات الإنشاء لا تدعمه في الإصدار 1: إنشاء منتج بنفس SKU يُرفض بـ 409.
أضف رابط استقبال من الإعدادات > رموز الربط > الويب هوك أو عبر POST /webhooks. الرابط يجب أن يكون https على عنوان عام (تُرفض العناوين الداخلية والمنافذ غير الافتراضية)، والحد 10 روابط للمتجر. مفتاح التوقيع يظهر عند الإنشاء أو التغيير، ويمكن لمن يملك صلاحية الإعدادات عرضه من اللوحة.
| الحدث | الصلاحية | متى |
|---|---|---|
order.created | orders:read | طلب جديد من أي قناة: المتجر، نقطة البيع، الطاولات، الحجز المسبق، MCP أو API. |
order.updated | orders:read | تعديل على الطلب: الأصناف أو الأسعار أو بيانات العميل والعنوان، أو جولة جديدة على طلب طاولة. |
order.status_changed | orders:read | تغيّرت حالة الطلب. الحقل data.previousStatus يحمل الحالة السابقة. |
order.paid | orders:read | سُجّل تحصيل ناجح في سجل المدفوعات وأصبح الطلب مدفوعاً (بوابة دفع، نقطة بيع، دفعة يدوية). |
order.cancelled | orders:read | دخل الطلب حالة إلغاء وأُعيد المخزون. |
customer.created | customers:read | إنشاء عميل: من اللوحة أو الاستيراد أو API أو الدفع أو نقطة البيع أو تسجيل المتجر. |
customer.updated | customers:read | تعديل ملف العميل. |
product.updated | catalog:read | إنشاء منتج أو تغيّر حقل فيه (لوحة، تعديل جماعي، استيراد، API). |
inventory.updated | catalog:read | حركة مخزون عبر سجل المخزون: تعديل يدوي، جرد، استلام، إلغاء أو مرتجع، API. |
الاشتراك عبر API يحتاج orders:write، بالإضافة إلى صلاحية القراءة لكل حدث (مثلاً customers:read لأحداث العملاء). حقل data هو نفس شكل المورد في مسار GET المقابل؛ inventory.updated يحمل حركة المخزون. اختبر الرابط بحدث ping عبر POST /webhooks/{id}/test.
POST /webhooks/shofni HTTP/1.1
Content-Type: application/json
User-Agent: Shofni-Webhooks/1.0
X-Shofni-Event: order.status_changed
X-Shofni-Delivery: dlv_4c1e9a0b7f2d83e6a5b1c09d
X-Shofni-Attempt: 1
X-Shofni-Signature: t=1760870400,v1=8f3a...c21e
{
"id": "evt_0c7e0b9d3a1f4e58b2c6d1a9e4f7b3c2",
"type": "order.status_changed",
"createdAt": "2026-10-19T10:40:00.000Z",
"data": { "id": "482913", "status": { ... }, "previousStatus": "PENDING", ... }
}التوقيع: v1 = hex(HMAC-SHA256(secret, t + "." + rawBody)). احسبه على الجسم الخام كما وصل، وقارن بمقارنة ثابتة الزمن، وارفض الطوابع الأقدم من 5 دقائق. أجب بأي حالة 2xx خلال 5 ثوانٍ؛ غير ذلك يُعاد إرساله بعد 1 د، 5 د، 30 د، 2 س، 6 س (6 محاولات). الإعادات تحمل نفس id للحدث ونفس X-Shofni-Delivery، فاستخدمه لمنع المعالجة المكررة. لا نتبع التحويلات (3xx).
import crypto from "node:crypto";
import express from "express";
import { localeAlternates } from "../seo";
const app = express();
const SECRET = process.env.SHOFNI_WEBHOOK_SECRET; // whsec_...
// Read the RAW body: the signature covers the exact bytes we sent.
app.post("/webhooks/shofni", express.raw({ type: "application/json" }), (req, res) => {
const header = req.get("X-Shofni-Signature") || "";
const parts = Object.fromEntries(header.split(",").map((p) => p.trim().split("=")));
const t = Number(parts.t);
const expected = crypto.createHmac("sha256", SECRET).update(`${t}.${req.body}`).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - t) <= 300;
const valid =
fresh &&
typeof parts.v1 === "string" &&
parts.v1.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
if (!valid) return res.status(400).send("invalid signature");
const event = JSON.parse(req.body.toString("utf8"));
// Retries reuse event.id: ignore an id you have already processed.
console.log(event.type, event.data.id);
res.sendStatus(200); // answer fast (5 s timeout); do heavy work async
});
app.listen(3000);<?php
$secret = getenv('SHOFNI_WEBHOOK_SECRET'); // whsec_...
$payload = file_get_contents('php://input'); // raw body
$header = $_SERVER['HTTP_X_SHOFNI_SIGNATURE'] ?? '';
$parts = [];
foreach (explode(',', $header) as $kv) {
[$k, $v] = array_pad(explode('=', trim($kv), 2), 2, '');
$parts[$k] = $v;
}
$t = (int) ($parts['t'] ?? 0);
$expected = hash_hmac('sha256', $t . '.' . $payload, $secret);
if (abs(time() - $t) > 300 || !hash_equals($expected, $parts['v1'] ?? '')) {
http_response_code(400);
exit('invalid signature');
}
$event = json_decode($payload, true);
// Retries reuse $event['id']: ignore an id you have already processed.
http_response_code(200);import hashlib, hmac, json, os, time
from flask import Flask, abort, request
app = Flask(__name__)
SECRET = os.environ["SHOFNI_WEBHOOK_SECRET"].encode() # whsec_...
@app.post("/webhooks/shofni")
def shofni_webhook():
payload = request.get_data() # raw bytes
header = request.headers.get("X-Shofni-Signature", "")
parts = dict(p.strip().split("=", 1) for p in header.split(",") if "=" in p)
t = int(parts.get("t", "0"))
expected = hmac.new(SECRET, f"{t}.".encode() + payload, hashlib.sha256).hexdigest()
if abs(time.time() - t) > 300 or not hmac.compare_digest(expected, parts.get("v1", "")):
abort(400)
event = json.loads(payload)
# Retries reuse event["id"]: ignore an id you have already processed.
return "", 200مولَّد من مواصفة OpenAPI المنشورة على /api/v1/openapi.json (يمكن استيرادها في Postman أو Insomnia). الأوصاف بالإنجليزية.
/ordersList ordersNewest first. Held table baskets are excluded.
| الحقل | الموضع | النوع | الوصف |
|---|---|---|---|
page | query | integer | |
limit | query | integer | |
status | query | string | Comma-separated status codes of this store. Unknown codes: 422 with `details.allowed`. |
payment_status | query | string | Comma-separated: paid, unpaid, failed, authorized, refunded_partial, refunded_full, refund_failed. |
created_after | query | string | ISO 8601, inclusive. |
created_before | query | string | ISO 8601, exclusive. A date without a time covers that whole UTC day. |
updated_after | query | string | ISO 8601, inclusive. Any change to the order moves `updatedAt`. |
q | query | string | Order number, customer name, phone or email, tracking number, product/variant SKU. |
channel | query | string | Comma-separated: store, pos, app, whatsapp, phone, manual, external. |
200A page of orders.401Missing or invalid token.403Token lacks the required scope.422Bad filter.429Rate limited (120 requests per minute per token).curl "https://shofniplus.com/api/v1/orders" \
-H "Authorization: Bearer $SHOFNI_TOKEN"{
"data": [
{
"id": "483920",
"number": "483920",
"status": {
"code": "PENDING",
"group": "NEW",
"name": {
"ar": "جديد",
"en": "New"
},
"customerName": {
"ar": "تم استلام طلبك",
"en": "Order received"
}
},
"paymentStatus": "paid",
"paymentMethod": {
"code": "card",
"group": "card"
},
"currency": "SAR",
"total": 27,
"amountPaid": 27,
"channel": "external",
"source": "api",
"serviceType": "delivery",
"itemCount": 2,
"customer": {
"id": "clx1cust",
"name": "سارة أحمد",
"phone": "966551112233",
"email": "[email protected]"
},
"trackingNumber": "AWB123",
"createdAt": "2026-10-19T08:00:00.000Z",
"updatedAt": "2026-10-19T08:05:00.000Z"
}
],
"pagination": {
"page": 1,
"limit": 25,
"total": 1,
"totalPages": 1
}
}/ordersCreate an orderCreates a PENDING order priced by the store (catalogue, variant surcharges, add-ons, membership prices, tax), the same path as the register. Tracked stock is deducted with stock movements and returned if the order is later cancelled; the store owner gets the usual new-order alert; restaurants get a kitchen ticket. Cash on delivery is refused (403) for customers the store blocked from COD. `Idempotency-Key`: optional. The same key within 24 hours returns the first order with 200 and `Idempotent-Replayed: true` instead of creating another.
| الحقل | الموضع | النوع | الوصف |
|---|---|---|---|
Idempotency-Key | header | string |
| الحقل | النوع | الوصف |
|---|---|---|
items* | OrderCreateItem[] | |
customer* | object | Either `{ id }` of an existing customer of this store, or `{ phone, name?, email? }`: the phone is matched against the store's customers in every spelling; a new customer is created (source API) when none matches. Existing names/emails are never overwritten, only filled in when empty. |
serviceType | "delivery" | "pickup" | "dine_in" | Default: delivery when shippingAddress is given, else pickup. Delivery needs shippingAddress.street or shippingAddress.shortAddress. |
shippingAddress | object | |
paymentMethod | "cod" | "cash" | "card" | "bank_transfer" | |
paid | boolean | true records a successful capture for the full total on the payment ledger (gateway "api"). Not allowed with cod. No money is charged by this API. |
note | string | Customer-facing order note. |
staffNote | string | Internal staff note added to the order. |
channel | string | Origin label. STORE, POS, APP, WHATSAPP or PHONE is stored as the order channel; any other text is kept as a free-form label. `source` is always "api". |
200Idempotent replay of an earlier request with the same key.201Created.401Missing or invalid token.403COD blocked for this customer, or missing scope.409Could not allocate an order number; retry.422Invalid input, unknown product, not enough stock, or a unit price was sent.429Rate limited (120 requests per minute per token).curl -X POST "https://shofniplus.com/api/v1/orders" \
-H "Authorization: Bearer $SHOFNI_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"items": [
{
"productId": "clx1burger",
"quantity": 2,
"note": "بدون بصل"
},
{
"sku": "CAKE-01",
"quantity": 1
}
],
"customer": {
"name": "سارة أحمد",
"phone": "+966551112233",
"email": "[email protected]"
},
"serviceType": "delivery",
"shippingAddress": {
"city": "الرياض",
"district": "العليا",
"street": "شارع التحلية",
"building": "12",
"shortAddress": "RRRD2929",
"latitude": 24.7,
"longitude": 46.68
},
"paymentMethod": "card",
"paid": true,
"note": "اتصل قبل الوصول",
"staffNote": "VIP",
"channel": "erp-x"
}'/orders/{id}Get one order| الحقل | الموضع | النوع | الوصف |
|---|---|---|---|
id* | path | string | Order id or its visible number. `123456`, `#A1B2C3` (URL-encode `#` as `%23`) and the legacy six-character suffix are accepted. |
200The order.401Missing or invalid token.403Token lacks the required scope.404No such order in this store (ids of other stores read as not found).429Rate limited (120 requests per minute per token).curl "https://shofniplus.com/api/v1/orders/{id}" \
-H "Authorization: Bearer $SHOFNI_TOKEN"{
"data": {
"id": "483920",
"number": "483920",
"status": {
"code": "PENDING",
"group": "NEW",
"name": {
"ar": "جديد",
"en": "New"
},
"customerName": {
"ar": "تم استلام طلبك",
"en": "Order received"
}
},
"paymentStatus": "paid",
"paymentMethod": {
"code": "card",
"group": "card"
},
"currency": "SAR",
"channel": "external",
"source": "api",
"serviceType": "delivery",
"totals": {
"subtotal": 27,
"listSubtotal": 27,
"discountTotal": 0,
"couponDiscount": 0,
"offerDiscount": 0,
"discounts": [],
"shipping": 0,
"codFee": 0,
"fees": [],
"tax": 0,
"taxInclusive": false,
"total": 27,
"amountPaid": 27,
"amountRefunded": 0,
"amountDue": 0
},
"lineItems": [
{
"productId": "clx1burger",
"variantId": null,
"sku": "BRG-01",
"name": "برجر",
"nameEn": "Burger",
"options": [],
"variantLabel": null,
"addons": [],
"note": "بدون بصل",
"quantity": 2,
"unitPrice": 10,
"listUnitPrice": 10,
"discount": 0,
"tax": 0,
"lineTotal": 20
}
],
"customer": {
"id": "clx1cust",
"name": "سارة أحمد",
"phone": "+966 55 111 2233",
"email": "[email protected]"
},
"shippingAddress": {
"name": "سارة أحمد",
"phone": "966551112233",
"country": null,
"region": null,
"city": "الرياض",
"district": "العليا",
"street": "شارع التحلية",
"building": "12",
"unit": null,
"postalCode": null,
"shortAddress": "RRRD2929",
"additionalNumber": null,
"coordinates": {
"lat": 24.7,
"lng": 46.68
}
},
"shipping": {
"method": "HOME_DELIVERY",
"carrierCode": "aramex",
"carrierName": "Aramex",
"trackingNumber": "AWB123",
"trackingUrl": "https://www.aramex.com/track/results?ShipmentNumber=AWB123",
"labelStatus": "ISSUED",
"shipmentStatus": null
},
"notes": {
"customer": "اتصل قبل الوصول",
"staff": [
{
"id": "clx1note",
"body": "VIP",
"authorName": "API: ERP",
"createdAt": "2026-10-19T08:00:00.000Z"
}
]
},
"tags": [],
"createdAt": "2026-10-19T08:00:00.000Z",
"updatedAt": "2026-10-19T08:05:00.000Z"
}
}/orders/{id}/statusMove an order to another statusRuns the store's own status engine, exactly as the dashboard does: allowed transitions, required reasons, kitchen ticket, stock return on cancellation, coupon/offer release, return paperwork, customer notification templates, audit trail (actor = the token). Refunds are not performed.
| الحقل | الموضع | النوع | الوصف |
|---|---|---|---|
id* | path | string | Order id or its visible number. `123456`, `#A1B2C3` (URL-encode `#` as `%23`) and the legacy six-character suffix are accepted. |
| الحقل | النوع | الوصف |
|---|---|---|
status* | string | A status code this store defines (see GET /order-statuses). |
reason | string | Required by some transitions (e.g. cancellation, return). |
notify | true | Only `true` (the default) is accepted: customer notifications follow the store's templates for the new status and cannot be suppressed through the API. |
200The updated order.401Missing or invalid token.403Token lacks the required scope.404No such order in this store (ids of other stores read as not found).409The order changed concurrently.422Unknown status, transition not allowed, reason required, or notify=false.429Rate limited (120 requests per minute per token).curl -X PATCH "https://shofniplus.com/api/v1/orders/{id}/status" \
-H "Authorization: Bearer $SHOFNI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"status": "CANCELLED",
"reason": "طلب العميل"
}'/orders/{id}/notesAdd an internal staff note| الحقل | الموضع | النوع | الوصف |
|---|---|---|---|
id* | path | string | Order id or its visible number. `123456`, `#A1B2C3` (URL-encode `#` as `%23`) and the legacy six-character suffix are accepted. |
| الحقل | النوع | الوصف |
|---|---|---|
body* | string | @name mentions an employee of this store. |
201Note added.401Missing or invalid token.403Token lacks the required scope.404No such order in this store (ids of other stores read as not found).422Empty note.429Rate limited (120 requests per minute per token).curl -X POST "https://shofniplus.com/api/v1/orders/{id}/notes" \
-H "Authorization: Bearer $SHOFNI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"body": "تم التحقق من العنوان"
}'{
"data": {
"id": "clx1note",
"orderId": "483920",
"body": "تم التحقق من العنوان",
"authorName": "API: ERP",
"createdAt": "2026-10-19T08:10:00.000Z",
"unresolvedMentions": []
}
}/orders/{id}/shippingRecord the carrier and tracking numberRecords a waybill that already exists at the carrier (manual ISSUED label, or confirms a label REQUESTED from the same carrier). Writes the shipment and order event and runs the store's LABEL_ISSUED automation. `trackingUrl` is derived from the carrier and cannot be set (422 if sent). An order that already has a live waybill returns 409: reissuing is done from the dashboard.
| الحقل | الموضع | النوع | الوصف |
|---|---|---|---|
id* | path | string | Order id or its visible number. `123456`, `#A1B2C3` (URL-encode `#` as `%23`) and the legacy six-character suffix are accepted. |
| الحقل | النوع | الوصف |
|---|---|---|
carrier* | string | Carrier code: fastcoo, redbox, aramex, smsa, naqel, spl, imile, oto, deliveryone, ... |
trackingNumber* | string | |
serviceCode | string |
200The updated order.401Missing or invalid token.403Token lacks the required scope.404No such order in this store (ids of other stores read as not found).409A live waybill already exists.422Unknown carrier or carrier switched off.429Rate limited (120 requests per minute per token).curl -X PATCH "https://shofniplus.com/api/v1/orders/{id}/shipping" \
-H "Authorization: Bearer $SHOFNI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"carrier": "aramex",
"trackingNumber": "AWB123"
}'/customersList customersNewest registration first. Customers merged into another record are excluded. `addresses` is not included in list rows.
| الحقل | الموضع | النوع | الوصف |
|---|---|---|---|
page | query | integer | |
limit | query | integer | |
q | query | string | Name, email or phone (local `05...` and international forms both match). |
created_after | query | string | ISO 8601; compared with the registration date. |
updated_after | query | string | Not supported for customers (no modification timestamp is kept): 422. |
200A page of customers.401Missing or invalid token.403Token lacks the required scope.422Bad filter (including updated_after).429Rate limited (120 requests per minute per token).curl "https://shofniplus.com/api/v1/customers" \
-H "Authorization: Bearer $SHOFNI_TOKEN"{
"data": [
{
"id": "clx1cust",
"number": 42,
"name": "خالد سعيد",
"phone": "966559998877",
"email": "[email protected]",
"emailVerified": false,
"marketingOptIn": false,
"language": "ar",
"country": null,
"status": "ACTIVE",
"source": "API",
"tags": [
"wholesale"
],
"loyaltyPoints": 120,
"walletBalance": 15.5,
"orderCount": 3,
"totalSpent": {
"SAR": 840
},
"lastOrderAt": "2026-10-18T19:22:00.000Z",
"registeredAt": "2026-09-01T10:00:00.000Z"
}
],
"pagination": {
"page": 1,
"limit": 25,
"total": 1,
"totalPages": 1
}
}/customersCreate a customerIdempotency-Key is not used here: the phone number is unique per store, so a retried create answers 409 with `details.customerId`.
| الحقل | النوع | الوصف |
|---|---|---|
phone* | string | E.164 (`+` and 8 to 15 digits; spaces and dashes are ignored). Stored in the store's canonical form. |
name | string | |
email | string | |
language | "ar" | "en" | |
tags | string[] | |
addresses | object[] |
201Created.401Missing or invalid token.403Token lacks the required scope.409This store already has the phone (any spelling) or the email.422Invalid input.429Rate limited (120 requests per minute per token).curl -X POST "https://shofniplus.com/api/v1/customers" \
-H "Authorization: Bearer $SHOFNI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "خالد",
"phone": "+966559998877",
"email": "[email protected]",
"tags": [
"wholesale"
],
"addresses": [
{
"label": "المنزل",
"city": "جدة",
"street": "شارع فلسطين",
"shortAddress": "JEDA1234"
}
]
}'{
"data": {
"id": "clx1cust",
"number": 42,
"name": "خالد سعيد",
"phone": "966559998877",
"email": "[email protected]",
"emailVerified": false,
"marketingOptIn": false,
"language": "ar",
"country": null,
"status": "ACTIVE",
"source": "API",
"tags": [
"wholesale"
],
"loyaltyPoints": 120,
"walletBalance": 15.5,
"orderCount": 3,
"totalSpent": {
"SAR": 840
},
"lastOrderAt": "2026-10-18T19:22:00.000Z",
"registeredAt": "2026-09-01T10:00:00.000Z",
"addresses": [
{
"id": "clx1addr",
"label": "المنزل",
"name": null,
"phone": null,
"country": null,
"region": null,
"city": "جدة",
"district": null,
"street": "شارع فلسطين",
"building": null,
"unit": null,
"postalCode": null,
"shortAddress": "JEDA1234",
"coordinates": null,
"isDefaultShipping": true,
"isDefaultBilling": true
}
]
}
}/customers/{id}Get one customer| الحقل | الموضع | النوع | الوصف |
|---|---|---|---|
id* | path | string | Customer id. |
200The customer, with addresses.401Missing or invalid token.403Token lacks the required scope.404No such customer in this store.429Rate limited (120 requests per minute per token).curl "https://shofniplus.com/api/v1/customers/{id}" \
-H "Authorization: Bearer $SHOFNI_TOKEN"{
"data": {
"id": "clx1cust",
"number": 42,
"name": "خالد سعيد",
"phone": "966559998877",
"email": "[email protected]",
"emailVerified": false,
"marketingOptIn": false,
"language": "ar",
"country": null,
"status": "ACTIVE",
"source": "API",
"tags": [
"wholesale"
],
"loyaltyPoints": 120,
"walletBalance": 15.5,
"orderCount": 3,
"totalSpent": {
"SAR": 840
},
"lastOrderAt": "2026-10-18T19:22:00.000Z",
"registeredAt": "2026-09-01T10:00:00.000Z",
"addresses": [
{
"id": "clx1addr",
"label": "المنزل",
"name": null,
"phone": null,
"country": null,
"region": null,
"city": "جدة",
"district": null,
"street": "شارع فلسطين",
"building": null,
"unit": null,
"postalCode": null,
"shortAddress": "JEDA1234",
"coordinates": null,
"isDefaultShipping": true,
"isDefaultBilling": true
}
]
}
}/customers/{id}Update a customer| الحقل | الموضع | النوع | الوصف |
|---|---|---|---|
id* | path | string | Customer id. |
| الحقل | النوع | الوصف |
|---|---|---|
name | string | null | |
email | string | null | Changing it resets emailVerified. |
phone | string | E.164. 409 if another customer of this store has it in any spelling. |
language | "ar" | "en" | null | |
tags | string[] | Replaces the current tag set. |
note | string | Appends one internal note to the customer (existing notes are not editable here). |
200The updated customer.401Missing or invalid token.403Token lacks the required scope.404No such customer in this store.409Phone/email belongs to another customer, or the record was merged.422Invalid input.429Rate limited (120 requests per minute per token).curl -X PATCH "https://shofniplus.com/api/v1/customers/{id}" \
-H "Authorization: Bearer $SHOFNI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "خالد سعيد",
"tags": [
"vip"
],
"note": "عميل جملة"
}'{
"data": {
"id": "clx1cust",
"number": 42,
"name": "خالد سعيد",
"phone": "966559998877",
"email": "[email protected]",
"emailVerified": false,
"marketingOptIn": false,
"language": "ar",
"country": null,
"status": "ACTIVE",
"source": "API",
"tags": [
"wholesale"
],
"loyaltyPoints": 120,
"walletBalance": 15.5,
"orderCount": 3,
"totalSpent": {
"SAR": 840
},
"lastOrderAt": "2026-10-18T19:22:00.000Z",
"registeredAt": "2026-09-01T10:00:00.000Z",
"addresses": [
{
"id": "clx1addr",
"label": "المنزل",
"name": null,
"phone": null,
"country": null,
"region": null,
"city": "جدة",
"district": null,
"street": "شارع فلسطين",
"building": null,
"unit": null,
"postalCode": null,
"shortAddress": "JEDA1234",
"coordinates": null,
"isDefaultShipping": true,
"isDefaultBilling": true
}
]
}
}/customers/{id}/ordersList a customer's ordersNewest first, open table baskets excluded. Requires customers:read.
| الحقل | الموضع | النوع | الوصف |
|---|---|---|---|
id* | path | string | Customer id. |
page | query | integer | |
limit | query | integer |
200A page of orders.401Missing or invalid token.403Token lacks the required scope.404No such customer in this store.429Rate limited (120 requests per minute per token).curl "https://shofniplus.com/api/v1/customers/{id}/orders" \
-H "Authorization: Bearer $SHOFNI_TOKEN"/productsList productscatalog:readNewest first. Archived products are excluded unless `status=archived`. Each product includes its option groups and variants.
| الحقل | الموضع | النوع | الوصف |
|---|---|---|---|
q | query | string | Search names, descriptions, SKUs, barcodes and tags (Arabic or English). |
category_id | query | string | Products whose primary category is this id. |
brand_id | query | string | Products of this brand. |
sku | query | string | Exact SKU (case-insensitive) on the product or any of its variants. |
status | query | "active" | "scheduled" | "draft" | "archived" | Lifecycle state. |
in_stock | query | boolean | true: tracked with stock > 0, or untracked. false: tracked and at or below 0. |
updated_after | query | string | ISO 8601; products changed at or after this instant (use for incremental sync). |
created_after | query | string | ISO 8601. |
created_before | query | string | ISO 8601. |
page | query | integer | Page number, from 1. |
limit | query | integer | Page size (max 100). |
200OK401Missing or invalid API token (`unauthorized`).403The token lacks the required scope (`forbidden`).422Bad filter value.429Rate limit exceeded (`rate_limited`, 120 requests per minute per token).curl "https://shofniplus.com/api/v1/products" \
-H "Authorization: Bearer $SHOFNI_TOKEN"/productsCreate a productcatalog:writeSlugs are generated from the names unless given. Idempotency-Key is not supported for products; a repeated create with the same `sku` is refused with 409.
| الحقل | النوع | الوصف |
|---|---|---|
name* | string | Arabic name (primary; the canonical URL is built from it). |
nameEn | string | null | |
description | string | null | Short Arabic description. |
descriptionEn | string | null | |
longDescription | string | null | Full Arabic body; simple HTML, sanitised on save. |
longDescriptionEn | string | null | |
slug | string | null | Arabic URL segment. Generated from the name when omitted. Changing it moves the public URL. |
slugEn | string | null | English URL alias. |
sku | string | null | Unique within the store (products and variants, case-insensitive). |
barcode | string | null | Unique within the store; EAN/UPC check digit validated. |
price* | number | Base selling price in the store currency. |
compareAtPrice | number | null | Was-price. Must be higher than `price`; null clears it. |
cost | number | null | |
status | "active" | "draft" | "archived" | `active` publishes and switches the product on; `archived` retires it and switches it off. |
categoryId | string | null | Primary category. Send this or `categoryIds`, not both. |
categoryIds | string[] | First id is the primary category, the rest are additional categories. |
brandId | string | null | |
tags | string[] | |
images | string[] | Image URLs, first is the main image. Entries already on the product and this store's own media URLs are kept as-is; any other public https URL is downloaded into the store's media library (JPEG, PNG, WebP, GIF, AVIF; max 8 MB; private/internal addresses refused). Replaces the whole list. |
trackStock | boolean | Whether stock is counted. Untracked products are never out of stock. |
isFeatured | boolean | |
isHidden | boolean | Reachable by direct link but not listed. |
options | object[] | Option groups. Replaces all groups and regenerates the variant matrix; variants for kept combinations keep their id, SKU and stock. |
stock | integer | Opening stock (turns tracking on). Written to the stock ledger as PURCHASE. Ignored with a warning when `options` create variants. |
201Created401Missing or invalid API token (`unauthorized`).403The token lacks the required scope (`forbidden`).409Duplicate SKU, barcode or slug.422Invalid field (details.field), foreign category/brand id, or an image URL that could not be imported.429Rate limit exceeded (`rate_limited`, 120 requests per minute per token).curl -X POST "https://shofniplus.com/api/v1/products" \
-H "Authorization: Bearer $SHOFNI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "تيشيرت قطن",
"nameEn": "Cotton T-shirt",
"price": 89,
"compareAtPrice": 120,
"sku": "TSH-001",
"status": "active",
"categoryIds": [
"clc1cat0001"
],
"images": [
"https://cdn.example.com/tshirt-front.jpg"
],
"trackStock": true,
"stock": 25
}'{
"data": {
"id": "clx8p2k0f0001",
"name": "تيشيرت قطن",
"nameEn": "Cotton T-shirt",
"slug": "تيشيرت-قطن",
"slugEn": "cotton-t-shirt",
"url": "https://mystore.shofniplus.com/products/%D8%AA%D9%8A%D8%B4%D9%8A%D8%B1%D8%AA-%D9%82%D8%B7%D9%86",
"description": "قطن مصري 100%",
"descriptionEn": "100% Egyptian cotton",
"longDescription": null,
"longDescriptionEn": null,
"status": "active",
"isActive": true,
"isHidden": false,
"isFeatured": false,
"sku": "TSH-001",
"barcode": null,
"price": 89,
"compareAtPrice": 120,
"effectivePrice": 79,
"onSale": true,
"cost": 35,
"currency": "SAR",
"trackStock": true,
"stock": 14,
"images": [
"https://mystore.shofniplus.com/uploads/clt0abc/1729250000000-a1b2-front.webp"
],
"categoryId": "clc1cat0001",
"categoryIds": [
"clc1cat0001",
"clc1cat0007"
],
"brand": {
"id": "clb1brand01",
"name": "نسيج",
"nameEn": "Naseej"
},
"tags": [
"صيفي"
],
"options": [
{
"id": "clv1opt0001",
"name": "المقاس",
"nameEn": "Size",
"required": true,
"values": [
{
"code": "m",
"label": "M",
"labelEn": null,
"priceDelta": 0
},
{
"code": "l",
"label": "L",
"labelEn": null,
"priceDelta": 5
}
]
}
],
"variants": [
{
"id": "cmb_01hx9k2v7",
"productId": "clx8p2k0f0001",
"sku": "TSH-RED-M",
"barcode": "6281234567897",
"optionValues": [
{
"group": "اللون",
"groupEn": "Colour",
"value": "أحمر",
"valueEn": "Red"
},
{
"group": "المقاس",
"groupEn": "Size",
"value": "M",
"valueEn": null
}
],
"price": 89,
"priceInherited": true,
"compareAtPrice": null,
"currency": "SAR",
"stock": 14,
"trackStock": true,
"isEnabled": true,
"image": null,
"weight": null,
"weightUnit": null,
"createdAt": "2026-10-01T09:12:00.000Z",
"updatedAt": "2026-10-18T14:03:11.000Z"
}
],
"weight": {
"net": 0.25,
"gross": 0.3,
"unit": "KG"
},
"createdAt": "2026-10-01T09:12:00.000Z",
"updatedAt": "2026-10-18T14:03:11.000Z"
}
}/products/{id}Get a productcatalog:read| الحقل | الموضع | النوع | الوصف |
|---|---|---|---|
id* | path | string | Product id, Arabic or English slug, or SKU (product or variant). |
200OK401Missing or invalid API token (`unauthorized`).403The token lacks the required scope (`forbidden`).404No such product in this store.429Rate limit exceeded (`rate_limited`, 120 requests per minute per token).curl "https://shofniplus.com/api/v1/products/{id}" \
-H "Authorization: Bearer $SHOFNI_TOKEN"{
"data": {
"id": "clx8p2k0f0001",
"name": "تيشيرت قطن",
"nameEn": "Cotton T-shirt",
"slug": "تيشيرت-قطن",
"slugEn": "cotton-t-shirt",
"url": "https://mystore.shofniplus.com/products/%D8%AA%D9%8A%D8%B4%D9%8A%D8%B1%D8%AA-%D9%82%D8%B7%D9%86",
"description": "قطن مصري 100%",
"descriptionEn": "100% Egyptian cotton",
"longDescription": null,
"longDescriptionEn": null,
"status": "active",
"isActive": true,
"isHidden": false,
"isFeatured": false,
"sku": "TSH-001",
"barcode": null,
"price": 89,
"compareAtPrice": 120,
"effectivePrice": 79,
"onSale": true,
"cost": 35,
"currency": "SAR",
"trackStock": true,
"stock": 14,
"images": [
"https://mystore.shofniplus.com/uploads/clt0abc/1729250000000-a1b2-front.webp"
],
"categoryId": "clc1cat0001",
"categoryIds": [
"clc1cat0001",
"clc1cat0007"
],
"brand": {
"id": "clb1brand01",
"name": "نسيج",
"nameEn": "Naseej"
},
"tags": [
"صيفي"
],
"options": [
{
"id": "clv1opt0001",
"name": "المقاس",
"nameEn": "Size",
"required": true,
"values": [
{
"code": "m",
"label": "M",
"labelEn": null,
"priceDelta": 0
},
{
"code": "l",
"label": "L",
"labelEn": null,
"priceDelta": 5
}
]
}
],
"variants": [
{
"id": "cmb_01hx9k2v7",
"productId": "clx8p2k0f0001",
"sku": "TSH-RED-M",
"barcode": "6281234567897",
"optionValues": [
{
"group": "اللون",
"groupEn": "Colour",
"value": "أحمر",
"valueEn": "Red"
},
{
"group": "المقاس",
"groupEn": "Size",
"value": "M",
"valueEn": null
}
],
"price": 89,
"priceInherited": true,
"compareAtPrice": null,
"currency": "SAR",
"stock": 14,
"trackStock": true,
"isEnabled": true,
"image": null,
"weight": null,
"weightUnit": null,
"createdAt": "2026-10-01T09:12:00.000Z",
"updatedAt": "2026-10-18T14:03:11.000Z"
}
],
"weight": {
"net": 0.25,
"gross": 0.3,
"unit": "KG"
},
"createdAt": "2026-10-01T09:12:00.000Z",
"updatedAt": "2026-10-18T14:03:11.000Z"
}
}/products/{id}Update a productcatalog:writeOnly the fields sent are written; null clears an optional field. Renaming keeps the URL. Price changes go to the price-change ledger and every save is a restorable revision. Stock is not set here: use PATCH /api/v1/inventory or PATCH /api/v1/variants/{id}.
| الحقل | الموضع | النوع | الوصف |
|---|---|---|---|
id* | path | string | Product id, Arabic or English slug, or SKU (product or variant). |
| الحقل | النوع | الوصف |
|---|---|---|
name | string | Arabic name (primary; the canonical URL is built from it). |
nameEn | string | null | |
description | string | null | Short Arabic description. |
descriptionEn | string | null | |
longDescription | string | null | Full Arabic body; simple HTML, sanitised on save. |
longDescriptionEn | string | null | |
slug | string | null | Arabic URL segment. Generated from the name when omitted. Changing it moves the public URL. |
slugEn | string | null | English URL alias. |
sku | string | null | Unique within the store (products and variants, case-insensitive). |
barcode | string | null | Unique within the store; EAN/UPC check digit validated. |
price | number | Base selling price in the store currency. |
compareAtPrice | number | null | Was-price. Must be higher than `price`; null clears it. |
cost | number | null | |
status | "active" | "draft" | "archived" | `active` publishes and switches the product on; `archived` retires it and switches it off. |
categoryId | string | null | Primary category. Send this or `categoryIds`, not both. |
categoryIds | string[] | First id is the primary category, the rest are additional categories. |
brandId | string | null | |
tags | string[] | |
images | string[] | Image URLs, first is the main image. Entries already on the product and this store's own media URLs are kept as-is; any other public https URL is downloaded into the store's media library (JPEG, PNG, WebP, GIF, AVIF; max 8 MB; private/internal addresses refused). Replaces the whole list. |
trackStock | boolean | Whether stock is counted. Untracked products are never out of stock. |
isFeatured | boolean | |
isHidden | boolean | Reachable by direct link but not listed. |
options | object[] | Option groups. Replaces all groups and regenerates the variant matrix; variants for kept combinations keep their id, SKU and stock. |
priceChangeReason | string | Stored on the price-change ledger. |
200OK401Missing or invalid API token (`unauthorized`).403The token lacks the required scope (`forbidden`).404No such product.409Duplicate SKU, barcode or slug.422Invalid field.429Rate limit exceeded (`rate_limited`, 120 requests per minute per token).curl -X PATCH "https://shofniplus.com/api/v1/products/{id}" \
-H "Authorization: Bearer $SHOFNI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"price": 79,
"compareAtPrice": 99,
"priceChangeReason": "ERP price sync"
}'/products/{id}Archive a productcatalog:writeNever deletes. Sets status `archived` and switches the product off; orders and reports keep it. PATCH `status: active` restores it.
| الحقل | الموضع | النوع | الوصف |
|---|---|---|---|
id* | path | string | Product id, Arabic or English slug, or SKU (product or variant). |
200Archived401Missing or invalid API token (`unauthorized`).403The token lacks the required scope (`forbidden`).404No such product.429Rate limit exceeded (`rate_limited`, 120 requests per minute per token).curl -X DELETE "https://shofniplus.com/api/v1/products/{id}" \
-H "Authorization: Bearer $SHOFNI_TOKEN"{
"data": {
"id": "clx8p2k0f0001",
"status": "archived",
"archived": true,
"deleted": false,
"alreadyArchived": false
}
}/products/{id}/variantsList a product's variantscatalog:readAll variants (max 500) in one page.
| الحقل | الموضع | النوع | الوصف |
|---|---|---|---|
id* | path | string | Product id, slug or SKU. |
200OK401Missing or invalid API token (`unauthorized`).403The token lacks the required scope (`forbidden`).404No such product.429Rate limit exceeded (`rate_limited`, 120 requests per minute per token).curl "https://shofniplus.com/api/v1/products/{id}/variants" \
-H "Authorization: Bearer $SHOFNI_TOKEN"/variants/{id}Update a variantcatalog:write| الحقل | الموضع | النوع | الوصف |
|---|---|---|---|
id* | path | string | Variant id. |
| الحقل | النوع | الوصف |
|---|---|---|
price | number | null | Null = inherit the product price. |
compareAtPrice | number | null | |
sku | string | null | |
barcode | string | null | |
stock | integer | New on-hand balance, written to the stock ledger (reason STOCKTAKE). |
trackStock | boolean | Send true to switch tracking on for an untracked product together with `stock`. |
200OK401Missing or invalid API token (`unauthorized`).403The token lacks the required scope (`forbidden`).404No such variant.409Duplicate SKU/barcode, or stock refused (`details.reason`: stock_not_tracked); other fields in the request were saved when `details.otherFieldsSaved` is true.422Invalid field.429Rate limit exceeded (`rate_limited`, 120 requests per minute per token).curl -X PATCH "https://shofniplus.com/api/v1/variants/{id}" \
-H "Authorization: Bearer $SHOFNI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"price": 95,
"sku": "TSH-RED-M",
"stock": 12
}'{
"data": {
"id": "cmb_01hx9k2v7",
"productId": "clx8p2k0f0001",
"sku": "TSH-RED-M",
"barcode": "6281234567897",
"optionValues": [
{
"group": "اللون",
"groupEn": "Colour",
"value": "أحمر",
"valueEn": "Red"
},
{
"group": "المقاس",
"groupEn": "Size",
"value": "M",
"valueEn": null
}
],
"price": 89,
"priceInherited": true,
"compareAtPrice": null,
"currency": "SAR",
"stock": 14,
"trackStock": true,
"isEnabled": true,
"image": null,
"weight": null,
"weightUnit": null,
"createdAt": "2026-10-01T09:12:00.000Z",
"updatedAt": "2026-10-18T14:03:11.000Z"
}
}/inventoryStock levelscatalog:readWith `sku`: the lines for those SKUs (unpaged) plus `notFound`. Without: every non-archived product, paged by product, one line per variant (or one per product without variants).
| الحقل | الموضع | النوع | الوصف |
|---|---|---|---|
sku | query | string | Comma-separated SKUs, max 200. |
page | query | integer | Page number, from 1. |
limit | query | integer | Page size (max 100). |
200OK401Missing or invalid API token (`unauthorized`).403The token lacks the required scope (`forbidden`).422Bad `sku` list.429Rate limit exceeded (`rate_limited`, 120 requests per minute per token).curl "https://shofniplus.com/api/v1/inventory" \
-H "Authorization: Bearer $SHOFNI_TOKEN"{
"data": {
"items": [
{
"productId": "clx8p2k0f0001",
"variantId": "cmb_01hx9k2v7",
"sku": "TSH-RED-M",
"barcode": null,
"name": "تيشيرت قطن",
"nameEn": "Cotton T-shirt",
"optionValues": [
{
"group": "المقاس",
"value": "M"
}
],
"trackStock": true,
"stock": 14,
"branches": [
{
"branchId": "clbr01",
"branchName": "الرياض",
"branchCode": "RUH",
"onHand": 10,
"reserved": 1,
"available": 9
}
]
}
],
"notFound": [
"UNKNOWN-1"
]
}
}/inventoryBulk set / adjust stockcatalog:write1 to 200 items, each applied independently through the stock ledger (a StockMovement row per change, actor = the token). The response is 200 with a per-item result; check `ok` on each. Stock never goes below zero.
| الحقل | النوع | الوصف |
|---|---|---|
items* | InventoryUpdateItem[] |
200Per-item results401Missing or invalid API token (`unauthorized`).403The token lacks the required scope (`forbidden`).422`items` missing, empty or over 200.429Rate limit exceeded (`rate_limited`, 120 requests per minute per token).curl -X PATCH "https://shofniplus.com/api/v1/inventory" \
-H "Authorization: Bearer $SHOFNI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"items": [
{
"sku": "TSH-RED-M",
"quantity": 40,
"mode": "set",
"reason": "STOCKTAKE"
},
{
"sku": "TSH-RED-L",
"quantity": -2,
"mode": "adjust",
"reason": "DAMAGE",
"note": "ERP doc 8812"
},
{
"variantId": "cmb_01hx9k2v8",
"quantity": 5,
"mode": "set",
"branchId": "clbr01"
}
]
}'{
"data": {
"results": [
{
"index": 0,
"ok": true,
"sku": "TSH-RED-M",
"productId": "clx8p2k0f0001",
"variantId": "cmb_01hx9k2v7",
"branchId": null,
"before": 14,
"after": 40,
"changed": true
},
{
"index": 1,
"ok": false,
"error": {
"code": "not_found",
"message": "No product or variant has SKU \"TSH-RED-L\"."
}
}
],
"summary": {
"total": 2,
"succeeded": 1,
"failed": 1
}
}
}/categoriesList categoriescatalog:readIn the merchant's order. Rows carry `parentId`; a full listing rebuilds the tree.
| الحقل | الموضع | النوع | الوصف |
|---|---|---|---|
parent_id | query | string | Children of this category; `root` for top-level categories. |
include_archived | query | boolean | Include archived categories. |
with_counts | query | boolean | Add productCount (whole subtree). |
page | query | integer | Page number, from 1. |
limit | query | integer | Page size (max 100). |
200OK401Missing or invalid API token (`unauthorized`).403The token lacks the required scope (`forbidden`).429Rate limit exceeded (`rate_limited`, 120 requests per minute per token).curl "https://shofniplus.com/api/v1/categories" \
-H "Authorization: Bearer $SHOFNI_TOKEN"/categoriesCreate a categorycatalog:write| الحقل | النوع | الوصف |
|---|---|---|
name* | string | Arabic name; required on create. |
nameEn | string | null | |
slug | string | null | |
slugEn | string | null | |
parentId | string | null | Null for top level. Refused if it would create a cycle or nest deeper than 4 levels. |
isHidden | boolean | |
description | string | null | |
descriptionEn | string | null | |
fullDescription | string | null | |
fullDescriptionEn | string | null | |
image | string | null | Image or icon URL. |
201Created401Missing or invalid API token (`unauthorized`).403The token lacks the required scope (`forbidden`).422Invalid field, unknown parent, cycle or too deep (details.reason).429Rate limit exceeded (`rate_limited`, 120 requests per minute per token).curl -X POST "https://shofniplus.com/api/v1/categories" \
-H "Authorization: Bearer $SHOFNI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "أحذية",
"nameEn": "Shoes",
"parentId": "clc1cat0001"
}'{
"data": {
"id": "clc1cat0001",
"name": "ملابس رجالية",
"nameEn": "Men's clothing",
"slug": "ملابس-رجالية",
"slugEn": "mens-clothing",
"parentId": null,
"sortOrder": 0,
"isHidden": false,
"isArchived": false,
"description": null,
"descriptionEn": null,
"fullDescription": null,
"fullDescriptionEn": null,
"image": null,
"createdAt": "2026-09-01T10:00:00.000Z"
}
}/categories/{id}Get a categorycatalog:read| الحقل | الموضع | النوع | الوصف |
|---|---|---|---|
id* | path | string | Category id. |
200OK401Missing or invalid API token (`unauthorized`).403The token lacks the required scope (`forbidden`).404No such category.429Rate limit exceeded (`rate_limited`, 120 requests per minute per token).curl "https://shofniplus.com/api/v1/categories/{id}" \
-H "Authorization: Bearer $SHOFNI_TOKEN"{
"data": {
"id": "clc1cat0001",
"name": "ملابس رجالية",
"nameEn": "Men's clothing",
"slug": "ملابس-رجالية",
"slugEn": "mens-clothing",
"parentId": null,
"sortOrder": 0,
"isHidden": false,
"isArchived": false,
"description": null,
"descriptionEn": null,
"fullDescription": null,
"fullDescriptionEn": null,
"image": null,
"createdAt": "2026-09-01T10:00:00.000Z"
}
}/categories/{id}Update a categorycatalog:writeOnly the fields sent are written. Renaming keeps the URL. Moving (parentId) moves the whole subtree.
| الحقل | الموضع | النوع | الوصف |
|---|---|---|---|
id* | path | string | Category id. |
| الحقل | النوع | الوصف |
|---|---|---|
name | string | Arabic name; required on create. |
nameEn | string | null | |
slug | string | null | |
slugEn | string | null | |
parentId | string | null | Null for top level. Refused if it would create a cycle or nest deeper than 4 levels. |
isHidden | boolean | |
description | string | null | |
descriptionEn | string | null | |
fullDescription | string | null | |
fullDescriptionEn | string | null | |
image | string | null | Image or icon URL. |
200OK401Missing or invalid API token (`unauthorized`).403The token lacks the required scope (`forbidden`).404No such category.422Invalid field.429Rate limit exceeded (`rate_limited`, 120 requests per minute per token).curl -X PATCH "https://shofniplus.com/api/v1/categories/{id}" \
-H "Authorization: Bearer $SHOFNI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"nameEn": "Footwear",
"isHidden": false
}'/brandsList brandscatalog:read| الحقل | الموضع | النوع | الوصف |
|---|---|---|---|
q | query | string | Name search (Arabic or English). |
include_archived | query | boolean | Include archived brands. |
page | query | integer | Page number, from 1. |
limit | query | integer | Page size (max 100). |
200OK401Missing or invalid API token (`unauthorized`).403The token lacks the required scope (`forbidden`).429Rate limit exceeded (`rate_limited`, 120 requests per minute per token).curl "https://shofniplus.com/api/v1/brands" \
-H "Authorization: Bearer $SHOFNI_TOKEN"/brands/{id}Get a brandcatalog:read| الحقل | الموضع | النوع | الوصف |
|---|---|---|---|
id* | path | string | Brand id or slug. |
200OK401Missing or invalid API token (`unauthorized`).403The token lacks the required scope (`forbidden`).404No such brand.429Rate limit exceeded (`rate_limited`, 120 requests per minute per token).curl "https://shofniplus.com/api/v1/brands/{id}" \
-H "Authorization: Bearer $SHOFNI_TOKEN"/storeStore informationcatalog:readName, currency, country, timezone, languages, domains and public contact details of the token's store.
200OK401Missing or invalid API token (`unauthorized`).403The token lacks the required scope (`forbidden`).429Rate limit exceeded (`rate_limited`, 120 requests per minute per token).curl "https://shofniplus.com/api/v1/store" \
-H "Authorization: Bearer $SHOFNI_TOKEN"/order-statusesOrder status catalogueorders:readEvery status code orders can carry, with its group and Arabic/English names. Stores that never customised statuses get the built-in set.
200OK401Missing or invalid API token (`unauthorized`).403The token lacks the required scope (`forbidden`).429Rate limit exceeded (`rate_limited`, 120 requests per minute per token).curl "https://shofniplus.com/api/v1/order-statuses" \
-H "Authorization: Bearer $SHOFNI_TOKEN"/webhooksList webhook endpointsorders:readRequires `orders:read`. Secrets are never listed.
200All endpoints of the store.401Missing or invalid API token.403The token lacks the required scope.429More than 120 requests in a trailing minute with this token.curl "https://shofniplus.com/api/v1/webhooks" \
-H "Authorization: Bearer $SHOFNI_TOKEN"/webhooksCreate a webhook endpointorders:writeRequires `orders:write`, plus the read scope of every subscribed event: `order.created` (orders:read), `order.updated` (orders:read), `order.status_changed` (orders:read), `order.paid` (orders:read), `order.cancelled` (orders:read), `customer.created` (customers:read), `customer.updated` (customers:read), `product.updated` (catalog:read), `inventory.updated` (catalog:read). At most 10 endpoints per store. The URL is checked at save time and before every delivery: https only, default port, and every address the host resolves to must be public.
| الحقل | النوع | الوصف |
|---|---|---|
url* | string | https only, default port, publicly routable host. |
events* | WebhookEventType[] | |
description | string | null | |
active | boolean |
201Created. `secret` is shown only now.401Missing or invalid API token.403The token lacks the required scope.409The request conflicts with the current state.422The request is malformed.429More than 120 requests in a trailing minute with this token.curl -X POST "https://shofniplus.com/api/v1/webhooks" \
-H "Authorization: Bearer $SHOFNI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhooks/shofni",
"events": [
"order.created",
"order.status_changed"
]
}'/webhooks/{id}Get a webhook endpoint with its recent deliveriesorders:readRequires `orders:read`. `recentDeliveries` holds the last 50 attempts, newest first.
| الحقل | الموضع | النوع | الوصف |
|---|---|---|---|
id* | path | string |
200The endpoint.401Missing or invalid API token.403The token lacks the required scope.404No such resource in this store.429More than 120 requests in a trailing minute with this token.curl "https://shofniplus.com/api/v1/webhooks/{id}" \
-H "Authorization: Bearer $SHOFNI_TOKEN"/webhooks/{id}Update a webhook endpoint or rotate its secretorders:writeRequires `orders:write` (and the read scope of any newly subscribed event).
| الحقل | الموضع | النوع | الوصف |
|---|---|---|---|
id* | path | string |
| الحقل | النوع | الوصف |
|---|---|---|
url | string | |
events | WebhookEventType[] | |
description | string | null | |
active | boolean | |
rotateSecret | boolean | Generate a new signing secret; the response then includes `secret`. |
200Updated. Includes `secret` only when `rotateSecret` was true.401Missing or invalid API token.403The token lacks the required scope.404No such resource in this store.422The request is malformed.429More than 120 requests in a trailing minute with this token.curl -X PATCH "https://shofniplus.com/api/v1/webhooks/{id}" \
-H "Authorization: Bearer $SHOFNI_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"active": false
}'/webhooks/{id}Delete a webhook endpointorders:writeRequires `orders:write`. Queued retries for it are dropped.
| الحقل | الموضع | النوع | الوصف |
|---|---|---|---|
id* | path | string |
200Deleted.401Missing or invalid API token.403The token lacks the required scope.404No such resource in this store.429More than 120 requests in a trailing minute with this token.curl -X DELETE "https://shofniplus.com/api/v1/webhooks/{id}" \
-H "Authorization: Bearer $SHOFNI_TOKEN"/webhooks/{id}/testSend a test `ping` eventorders:writeRequires `orders:write`. Sends a signed `ping` now and reports the result. Logged, never retried.
| الحقل | الموضع | النوع | الوصف |
|---|---|---|---|
id* | path | string |
200Attempt result (delivered or not).401Missing or invalid API token.403The token lacks the required scope.404No such resource in this store.429More than 120 requests in a trailing minute with this token.curl -X POST "https://shofniplus.com/api/v1/webhooks/{id}/test" \
-H "Authorization: Bearer $SHOFNI_TOKEN"