العودة إلى NShop
    API v1

    واجهة برمجة متجر NShop

    ابنِ متجرك الإلكتروني فوق حسابك في NShop: اقرأ المنتجات والشحن وإعدادات المتجر، وأنشئ الطلبات وتتبعها عبر واجهة REST بسيطة وآمنة.

    العربية: ملخص سريع

    • واجهة NShop البرمجية تتيح لك بناء متجر إلكتروني خاص بك يقرأ المنتجات والإعدادات من حسابك ويرسل الطلبات إليه، بدل الاتصال المباشر بقاعدة البيانات.
    • للحصول على مفتاح: سجّل الدخول كمدير، افتح الإضافات ثم المتجر الإلكتروني، ثم تبويب API، واضغط إنشاء مفتاح واختر الصلاحيات.
    • المفتاح يبدأ بـ nsk_live_ ويظهر مرة واحدة فقط، فاحفظه فوراً في متغيرات البيئة على الخادم ولا تضعه في كود المتصفح.
    • أرسل المفتاح في الترويسة: Authorization: Bearer nsk_live_... (أو X-API-Key).
    • الصلاحيات: store:read للإعدادات والمدن، catalog:read للمنتجات والتصنيفات، orders:write لإنشاء الطلبات، orders:read لتتبعها.
    • العنوان الأساسي: https://nshop.naqsh-tech.com/api/v1، والمواصفات الكاملة بصيغة OpenAPI على /api/v1/openapi.json.
    • عند إنشاء طلب أرسل رقم المنتج والكمية فقط؛ الأسعار والعروض والشحن تُحسب في الخادم، ولا يُقبل أي سعر أو خصم أو ضريبة من العميل.
    • رقم الجوال يجب أن يكون فلسطينياً (05XXXXXXXX)، واستخدم الترويسة Idempotency-Key عند إعادة المحاولة حتى لا يتكرر الطلب.
    • لتتبع طلب يلزم رقم الطلب وآخر 4 أرقام من هاتف العميل. وحالة الطلب يغيّرها صاحب المتجر من لوحة NShop.
    • للدعم تواصل عبر واتساب على 00972595856529 وأرسل معرّف الطلب X-Request-Id من استجابة الخطأ.

    Get your API key

    1. Sign in to NShop as an admin and make sure the E-commerce add-on is enabled.
    2. Open Add-ons, E-commerce and select the API tab.
    3. Choose Create API key, name it (for example "Production storefront") and tick only the scopes it needs.
    4. Copy the key now. It starts with nsk_live_ and is shown only once; NShop stores just a hash.

    Each company can hold up to 10 active keys. Revoke a key at any time from the same tab; it stops working immediately.

    Authentication

    Send the key in the Authorization header (or X-API-Key). The company is resolved from the key, so you never pass a company id. Call the API from your server, never from browser code.

    http
    Authorization: Bearer nsk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    # or
    X-API-Key: nsk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    ScopeGrants
    store:readStore configuration, theme, shipping cities and banner statements.
    catalog:readCategories, products and best sellers.
    orders:writePlace orders.
    orders:readTrack orders (requires the customer's phone_last4).

    Key safety. Store keys in environment variables, never commit them, use one key per application and give it the minimum scopes. A storefront normally needs store:read, catalog:read, orders:write and orders:read. If a key leaks, revoke it and create a new one.

    Quickstart

    Check the connection (no key needed), then list products with your key.

    bash
    curl https://nshop.naqsh-tech.com/api/v1/health
    bash
    curl "https://nshop.naqsh-tech.com/api/v1/products?limit=12&page=1" \
      -H "Authorization: Bearer $NSHOP_API_KEY"

    The same call from Node.js / Next.js server code:

    javascript
    const res = await fetch("https://nshop.naqsh-tech.com/api/v1/products?limit=12", {
      headers: { Authorization: "Bearer " + process.env.NSHOP_API_KEY },
    });
    if (!res.ok) throw new Error((await res.json()).error.message);
    const { data: products, meta } = await res.json();
    console.log(products.length, "of", meta.total);

    Placing an order (see the order guide):

    bash
    curl -X POST "https://nshop.naqsh-tech.com/api/v1/orders" \
      -H "Authorization: Bearer $NSHOP_API_KEY" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: 7d9e4f1a-checkout-1042" \
      -d '{
        "customer_name": "Ahmad Saleh",
        "customer_phone": "0599123456",
        "customer_city": "Ramallah",
        "customer_address": "Al-Masyoun, Street 5, building 12",
        "items": [{ "product_id": "6f1c1c1e-5a40-4e0d-9d52-1d1a0b8f3a11", "quantity": 2 }]
      }'

    Prefer a typed client? Copy this single file into your project. It wraps every endpoint, throws a typed NShopApiError and keeps the same cache tags your storefront already uses.

    nshop-client.ts (copy-paste)
    typescript
    // nshop-client.ts - typed, server-side client for the NShop Storefront API (v1).
    // Env: NSHOP_API_BASE_URL (e.g. https://nshop.naqsh-tech.com) and NSHOP_API_KEY (nsk_live_...).
    // Keep the key on the server. Never ship it to the browser.
    
    const BASE_URL = process.env.NSHOP_API_BASE_URL ?? "https://nshop.naqsh-tech.com";
    const API_KEY = process.env.NSHOP_API_KEY ?? "";
    
    export type ApiErrorCode =
      | "invalid_api_key" | "insufficient_scope" | "ecommerce_disabled" | "rate_limited"
      | "validation_error" | "not_found" | "conflict" | "insufficient_stock"
      | "product_not_available" | "idempotency_conflict" | "internal_error";
    
    export class NShopApiError extends Error {
      constructor(
        public status: number,
        public code: ApiErrorCode,
        message: string,
        public details?: Record<string, unknown>,
        public requestId?: string,
        public retryAfter?: number,
      ) {
        super(message);
        this.name = "NShopApiError";
      }
    }
    
    export interface PageMeta { total: number; limit: number; page: number; has_more: boolean }
    export interface Offer { id: string; kind: "percent" | "fixed"; value: number; ends_at: string | null }
    export interface Product {
      id: string; name: string; sku: string | null; barcode: string | null; description: string | null;
      category_id: string | null; category_name: string | null; images: string[];
      price: number; compare_at_price: number | null; offer: Offer | null;
      in_stock: boolean; stock_quantity: number; updated_at: string;
    }
    export interface Category {
      id: string; name: string; description: string | null; parent_id: string | null;
      level: number | null; sort_order: number | null; icon_url: string | null; children?: Category[];
    }
    export interface ShippingCity { id: string; city_name: string; shipping_cost: number; sort_order: number | null }
    export interface BannerStatement { id: string; body: string; sort_order: number | null }
    export interface Store {
      company: Record<string, unknown>;
      settings: Record<string, unknown> | null;
      theme: Record<string, unknown> | null;
    }
    export interface CreateOrderInput {
      customer_name: string; customer_phone: string; customer_phone2?: string; customer_email?: string;
      customer_city: string; customer_address: string; order_note?: string;
      items: Array<{ product_id: string; quantity: number }>;
    }
    export interface OrderCreated {
      id: string; order_number: string; status: string; subtotal: number;
      shipping_amount: number; discount_amount: number; total_amount: number;
    }
    
    interface RequestOptions {
      method?: "GET" | "POST";
      body?: unknown;
      headers?: Record<string, string>;
      /** Next.js fetch cache: seconds to cache and tags for revalidateTag(). */
      revalidate?: number | false;
      tags?: string[];
    }
    
    async function request<T>(path: string, opts: RequestOptions = {}): Promise<{ data: T; meta?: PageMeta }> {
      const init: RequestInit & { next?: { revalidate: number; tags?: string[] } } = {
        method: opts.method ?? "GET",
        headers: {
          Authorization: "Bearer " + API_KEY,
          ...(opts.body ? { "Content-Type": "application/json" } : {}),
          ...opts.headers,
        },
        body: opts.body ? JSON.stringify(opts.body) : undefined,
      };
      if (opts.method === "POST" || opts.revalidate === false) init.cache = "no-store";
      else if (opts.revalidate !== undefined) init.next = { revalidate: opts.revalidate, tags: opts.tags };
    
      const res = await fetch(BASE_URL + path, init);
      const json = await res.json().catch(() => null);
      if (!res.ok) {
        const e = json?.error ?? {};
        throw new NShopApiError(
          res.status,
          e.code ?? "internal_error",
          e.message ?? "Request failed",
          e.details,
          res.headers.get("X-Request-Id") ?? undefined,
          Number(res.headers.get("Retry-After")) || undefined,
        );
      }
      return json;
    }
    
    // Same cache tags / TTLs as the old unstable_cache layer, so /api/revalidate keeps working.
    const CONFIG = { revalidate: 3600, tags: ["storefront-config"] };
    const CATALOG = { revalidate: 600, tags: ["storefront-catalog"] };
    
    function qs(params: Record<string, string | number | undefined>): string {
      const q = new URLSearchParams();
      for (const [k, v] of Object.entries(params)) if (v !== undefined && v !== "") q.set(k, String(v));
      const s = q.toString();
      return s ? "?" + s : "";
    }
    
    export const nshop = {
      getStore: async () => (await request<Store>("/api/v1/store", CONFIG)).data,
      getShippingCities: async () => (await request<ShippingCity[]>("/api/v1/shipping-cities", CONFIG)).data,
      getBannerStatements: async () => (await request<BannerStatement[]>("/api/v1/banner-statements", CONFIG)).data,
      getCategories: async (tree = false) =>
        (await request<Category[]>("/api/v1/categories" + qs({ tree: tree ? "true" : undefined }), CATALOG)).data,
      listProducts: (p: { page?: number; limit?: number; categoryId?: string; q?: string; ids?: string[]; updatedSince?: string } = {}) =>
        request<Product[]>(
          "/api/v1/products" +
            qs({ page: p.page, limit: p.limit, category_id: p.categoryId, q: p.q, ids: p.ids?.join(","), updated_since: p.updatedSince }),
          CATALOG,
        ),
      getProduct: async (id: string) => (await request<Product>("/api/v1/products/" + id, CATALOG)).data,
      getBestSellers: async (limit = 10, days = 90) =>
        (await request<Array<Product & { units_sold: number }>>("/api/v1/best-sellers" + qs({ limit, days }), CATALOG)).data,
    
      /** Place an order. Send only product ids and quantities. Reuse idempotencyKey when retrying the same checkout. */
      createOrder: async (input: CreateOrderInput, idempotencyKey: string) =>
        (await request<OrderCreated>("/api/v1/orders", {
          method: "POST",
          body: input,
          headers: { "Idempotency-Key": idempotencyKey },
        })).data,
    
      /** Track an order. The customer must supply the last four digits of their phone. */
      getOrder: async (orderNumber: string, phoneLast4: string) =>
        (await request<Record<string, unknown>>(
          "/api/v1/orders/" + encodeURIComponent(orderNumber) + qs({ phone_last4: phoneLast4 }),
          { revalidate: false },
        )).data,
    };
    

    Conventions

    Envelope. Success is { "data": ... }, plus meta for paginated lists. Errors are always:

    json
    { "error": { "code": "validation_error", "message": "Invalid request.", "details": { "issues": [{ "path": "customer_city", "message": "Unknown city" }] } } }
    CodeHTTPMeaning
    invalid_api_key401Key missing, malformed, unknown, revoked or expired.
    insufficient_scope403The key lacks the scope this endpoint needs (details.required_scope).
    ecommerce_disabled403E-commerce is not enabled for the account.
    rate_limited429Too many requests. Wait Retry-After seconds.
    validation_error400Bad body, query or path. details.issues lists the fields.
    not_found404Unknown resource (or wrong phone_last4 for an order).
    insufficient_stock409Reserved for when stock enforcement is enabled; not returned today.
    conflict409Order number collision. Retry.
    idempotency_conflict409Idempotency-Key reused with a different body.
    product_not_available422A product cannot be sold (inactive or not published).
    internal_error500 / 502Unexpected failure. Quote X-Request-Id to support.

    Pagination. /products takes limit (1-100, default 24) and page or offset. meta returns { total, limit, page, has_more }.

    Caching. Read endpoints send Cache-Control: private, max-age=…: store configuration 300 s, catalog 60 s. Orders are never cached. Cache on your side with fetch tags and revalidate when the shop changes.

    Rate limits. 120 requests/minute per key and endpoint (order creation: 20/minute; public endpoints: 60/minute per IP). Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset; a 429 adds Retry-After (seconds).

    Request ids. Each response includes X-Request-Id. Log it; support can trace the request with it.

    Idempotency. Send Idempotency-Key (8-128 chars) on POST /orders. A retry with the same key and body returns the original response with Idempotent-Replayed: true; the same key with a different body returns 409.

    Endpoint reference

    Machine-readable spec: /api/v1/openapi.json (OpenAPI 3.1, import it into Postman, Insomnia or a code generator).

    GET
    /api/v1/healthLiveness probe

    Public. No API key needed. Rate limited per IP (60/min).

    bash
    curl "https://nshop.naqsh-tech.com/api/v1/health"
    json
    {
      "data": {
        "status": "ok",
        "version": "v1",
        "time": "2026-10-10T09:00:00.000Z"
      }
    }

    Responses: 200429

    GET
    /api/v1/storeStore configuration
    store:read

    Public company fields, e-commerce settings and the resolved theme. Cached for 300 s. Requires scope `store:read`.

    bash
    curl "https://nshop.naqsh-tech.com/api/v1/store" \
      -H "Authorization: Bearer $NSHOP_API_KEY"

    Responses: 200401403404429500

    GET
    /api/v1/shipping-citiesShipping cities and their cost
    store:read

    Use `city_name` as `customer_city` when placing an order. Cached 300 s. Requires `store:read`.

    bash
    curl "https://nshop.naqsh-tech.com/api/v1/shipping-cities" \
      -H "Authorization: Bearer $NSHOP_API_KEY"
    json
    {
      "data": [
        {
          "id": "6f1c1c1e-5a40-4e0d-9d52-1d1a0b8f3a11",
          "city_name": "Ramallah",
          "shipping_cost": 20,
          "sort_order": 0
        }
      ]
    }

    Responses: 200401403429500

    GET
    /api/v1/banner-statementsBanner statements
    store:read

    Active announcement texts in display order. Cached 300 s. Requires `store:read`.

    bash
    curl "https://nshop.naqsh-tech.com/api/v1/banner-statements" \
      -H "Authorization: Bearer $NSHOP_API_KEY"

    Responses: 200401403429500

    GET
    /api/v1/categoriesCategories
    catalog:read

    Active categories, flat by default or nested with `tree=true`. Cached 60 s. Requires `catalog:read`.

    NameInTypeDescription
    treequerystringReturn a nested tree (`children`).
    bash
    curl "https://nshop.naqsh-tech.com/api/v1/categories" \
      -H "Authorization: Bearer $NSHOP_API_KEY"

    Responses: 200400401403429500

    GET
    /api/v1/productsList products
    catalog:read

    Active products with live price (offers applied) and an informational stock count, assembled in one database call. Paginate with `page` or `offset`; filter with `category_id`, `q`, `ids` or `updated_since`. Cached 60 s. Requires `catalog:read`.

    NameInTypeDescription
    limitqueryinteger
    pagequeryinteger1-based page number.
    offsetqueryintegerAlternative to `page`.
    category_idquerystring (uuid)
    qquerystringSearch text. Case-insensitive substring match on name, SKU and barcode.
    updated_sincequerystring (date-time)Full ISO-8601 datetime, e.g. `2026-10-10T08:00:00Z` or `2026-09-21T06:45:26+00:00`. A date-only value such as `2026-09-21` returns 400 `validation_error`. Returns only products whose `updated_at` is on or after it (name-sorted like any listing). Use it for incremental sync. Products that were unpublished or deleted simply stop appearing and are not reported, so also do a full refresh periodically. An invalid value returns 400 `validation_error`.
    idsquerystringComma-separated product UUIDs (max 100).
    bash
    curl "https://nshop.naqsh-tech.com/api/v1/products" \
      -H "Authorization: Bearer $NSHOP_API_KEY"
    json
    {
      "data": [],
      "meta": {
        "total": 0,
        "limit": 24,
        "page": 1,
        "has_more": false
      }
    }

    Responses: 200400401403429500

    GET
    /api/v1/products/{id}Get one product
    catalog:read

    Accepts the product id (UUID). Cached 60 s. Requires `catalog:read`.

    NameInTypeDescription
    id *pathstring (uuid)
    bash
    curl "https://nshop.naqsh-tech.com/api/v1/products/<id>" \
      -H "Authorization: Bearer $NSHOP_API_KEY"

    Responses: 200400401403404429500

    GET
    /api/v1/best-sellersBest-selling products
    catalog:read

    Ranked by units sold in the last `days` days (cancelled and refunded orders excluded). Each product carries `units_sold`. Cached 60 s. Requires `catalog:read`.

    NameInTypeDescription
    limitqueryinteger
    daysqueryinteger
    bash
    curl "https://nshop.naqsh-tech.com/api/v1/best-sellers" \
      -H "Authorization: Bearer $NSHOP_API_KEY"

    Responses: 200400401403429500

    POST
    /api/v1/ordersPlace an order
    orders:write

    Creates a cash-on-delivery order. Send only product ids and quantities: prices, discounts and tax are never accepted (unknown fields are rejected). Every line is priced server-side from live data including active offers, shipping comes from the city's configured cost. Stock is not checked at this endpoint today: the shop reviews orders in NShop. Rate limited to 20/min. Requires `orders:write`.

    NameInTypeDescription
    bash
    curl -X POST "https://nshop.naqsh-tech.com/api/v1/orders" \
      -H "Authorization: Bearer $NSHOP_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
      "customer_name": "Ahmad Saleh",
      "customer_phone": "0599123456",
      "customer_city": "Ramallah",
      "customer_address": "Al-Masyoun, Street 5, building 12",
      "order_note": "Call before delivery",
      "items": [
        {
          "product_id": "6f1c1c1e-5a40-4e0d-9d52-1d1a0b8f3a11",
          "quantity": 2
        }
      ]
    }'
    json
    {
      "data": {
        "id": "6f1c1c1e-5a40-4e0d-9d52-1d1a0b8f3a11",
        "order_number": "ORD-2026-A1B2C3",
        "status": "pending",
        "subtotal": 100,
        "shipping_amount": 20,
        "discount_amount": 0,
        "total_amount": 120
      }
    }

    Responses: 201400401403409422429500502

    GET
    /api/v1/orders/{orderNumber}Track an order
    orders:read

    Requires the last four digits of the customer's phone (`phone_last4`). A wrong or missing match answers exactly like an unknown order number (404), so order numbers cannot be probed. The phone is returned masked. Requires `orders:read`.

    NameInTypeDescription
    orderNumber *pathstring
    phone_last4 *querystring
    bash
    curl "https://nshop.naqsh-tech.com/api/v1/orders/<orderNumber>?phone_last4=<phone_last4>" \
      -H "Authorization: Bearer $NSHOP_API_KEY"

    Responses: 200400401403404429500

    Object reference

    Field-by-field description of every object the API accepts or returns. A field marked required is always present; | null means the value can be null. Ignore fields you do not know: new fields can be added to responses at any time (see versioning).

    ProductA sellable product as it should appear in the storefront: live price with offers applied and an informational stock count.

    A sellable product as it should appear in the storefront: live price with offers applied and an informational stock count.

    FieldTypeRequiredDescriptionExample
    idstring (uuid)noProduct id. Use it as `items[].product_id` when ordering.6f1c1c1e-5a40-4e0d-9d52-1d1a0b8f3a11
    namestringnoVitamin C cream
    skustring | nullnoStock keeping unit.CRM-001
    barcodestring | nullno6290000000012
    descriptionstring | nullnoProduct description (may be null).
    category_idstring | null (uuid)no
    category_namestring | nullnoSkin care
    imagesstring (uri)[]noPublic image URLs, main image first. Empty array when the product has no images.["https://assets.example.com/p/cream-1.jpg"]
    pricenumbernoEffective unit price AFTER the best active offer. This is what the customer pays per unit; show and charge this.42.5
    compare_at_pricenumber | nullnoOriginal list price, set only when an offer applies (use it for a crossed-out price). Null when there is no offer.50
    offerOffer | nullnoThe offer that produced `price`, or null.
    in_stockbooleannoInformational, for display: `true` when `stock_quantity` is greater than 0.true
    stock_quantityintegernoInformational, for display: the sum of (quantity - reserved_stock) over all of the company's shops and warehouses, never negative. The API does not currently reject orders that exceed stock, so do not rely on it as a guarantee.18
    updated_atstring (date-time)noLatest change of the product or of its storefront cache entry. Use it with `updated_since` to sync incrementally.2026-09-21T06:45:26.062544+00:00
    OfferThe best active promotion applied to a product

    The best active promotion applied to a product. The discount is already included in `Product.price`.

    FieldTypeRequiredDescriptionExample
    idstring (uuid)no6f1c1c1e-5a40-4e0d-9d52-1d1a0b8f3a11
    kindstringno`percent`: `value` is a percentage off the list price. `fixed`: `value` is an amount off the list price (never below zero). one of: percent, fixedpercent
    valuenumbernoPercent or amount, depending on `kind`.15
    ends_atstring | null (date-time)noWhen the offer ends; null for open-ended offers. Use it for countdowns.2026-10-31T21:00:00.000Z
    CategoryA product category

    A product category. With `tree=true`, children are nested.

    FieldTypeRequiredDescriptionExample
    idstring (uuid)no6f1c1c1e-5a40-4e0d-9d52-1d1a0b8f3a11
    namestringnoSkin care
    descriptionstring | nullno
    parent_idstring | null (uuid)noParent category id, or null for a top-level category.
    levelinteger | nullnoDepth in the tree (0 = top level).0
    sort_orderinteger | nullnoDisplay order among siblings (ascending).1
    icon_urlstring | null (uri)noCategory icon image URL.
    childrenCategory[]noPresent only with `tree=true`.
    ShippingCityA city the shop delivers to, with its delivery cost.

    A city the shop delivers to, with its delivery cost.

    FieldTypeRequiredDescriptionExample
    idstring (uuid)no6f1c1c1e-5a40-4e0d-9d52-1d1a0b8f3a11
    city_namestringnoSend this exact value (case-insensitive) as `customer_city` when ordering.Ramallah
    shipping_costnumbernoDelivery cost in the shop currency. Added to the order total by the server.20
    sort_orderinteger | nullnoDisplay order (ascending).0
    BannerStatementA short announcement line shown above the store.

    A short announcement line shown above the store.

    FieldTypeRequiredDescriptionExample
    idstring (uuid)no6f1c1c1e-5a40-4e0d-9d52-1d1a0b8f3a11
    bodystringnoThe text to display.Free delivery over 200 ILS
    sort_orderinteger | nullnoDisplay order (ascending).0
    StoreEverything needed to brand and configure a storefront, in one call.

    Everything needed to brand and configure a storefront, in one call.

    FieldTypeRequiredDescriptionExample
    companyCompanyno
    settingsSettingsno
    themeThemeno
    CompanyPublic storefront identity of the company (`Store.company`)

    Public storefront identity of the company (`Store.company`). Only these whitelisted fields are ever returned; internal data is never exposed.

    FieldTypeRequiredDescriptionExample
    namestringnoLegal/business name.Al-Baraka Store
    ecommerce_store_namestring | nullnoDisplay name of the online store. Prefer this over `name` in the UI.Al-Baraka Online
    ecommerce_descriptionstring | nullnoShort store description for the home page and SEO.
    ecommerce_logo_urlstring | null (uri)noLogo image URL.
    ecommerce_banner_urlstring | null (uri)noMain hero banner image URL.
    ecommerce_small_banner_2_urlstring | null (uri)noSecondary banner image URL.
    ecommerce_small_banner_3_urlstring | null (uri)noThird banner image URL.
    ecommerce_whatsappstring | nullnoStore WhatsApp number for customer chat.970599123456
    ecommerce_social_linksobjectnoSocial profile links keyed by network (e.g. `facebook`, `instagram`).{"instagram":"https://instagram.com/albaraka"}
    ecommerce_contact_infoobject | nullnoContact details (phone, email, address) as configured by the shop.
    ecommerce_policiesobjectnoPolicy texts (shipping, returns, privacy) as configured by the shop.
    ecommerce_storefront_urlstring | null (uri)noPublic URL of the storefront.
    ecommerce_currencystring | nullnoCurrency code for the online store. Use this for display; fall back to `currency` when null.ILS
    currencystring | nullnoThe company's accounting currency code (ILS, USD or JOD).ILS
    ecommerce_languagestring | nullnoStorefront language code.ar
    ecommerce_themestring | nullnoName of the selected theme (matches `Theme.name`).modern
    SettingsE-commerce settings row (`Store.settings`)

    E-commerce settings row (`Store.settings`). `null` when the shop has not saved any settings yet.

    FieldTypeRequiredDescriptionExample
    theme_configobject | nullnoPer-store theme overrides (colors, fonts, layout options).
    payment_methodsobject | array | nullnoPayment options the shop configured. Orders created through the API are cash on delivery.
    shipping_optionsobject | array | nullnoShipping options configured by the shop. Costs per city come from `/shipping-cities`.
    currencystring | nullnoILS
    languagestring | nullnoar
    timezonestring | nullnoIANA timezone of the shop.Asia/Hebron
    business_hoursobject | nullnoOpening hours by weekday.
    social_linksobject | nullnoSocial links configured in settings.
    seo_settingsobject | nullnoSEO defaults (title, description, keywords).
    updated_atstring | null (date-time)noLast time the settings changed.
    ThemeThe resolved theme (`Store.theme`)

    The resolved theme (`Store.theme`). `null` when the store has no active theme.

    FieldTypeRequiredDescriptionExample
    namestringnoTheme identifier (matches `Company.ecommerce_theme`).modern
    display_namestringnoHuman-readable theme name.Modern
    descriptionstring | nullno
    configobjectnoTheme defaults (colors, typography, layout).
    preview_imagesarray | nullnoPreview screenshots.
    is_premiumboolean | nullnoWhether the theme is a premium theme.
    CreateOrderRequestBody of `POST /api/v1/orders`

    Body of `POST /api/v1/orders`. Unknown fields (for example `unit_price`, `total_price`, `discount_amount`, `tax_amount`, `payment_method`) are rejected with 400.

    FieldTypeRequiredDescriptionExample
    customer_namestringyesCustomer full name. Control characters are stripped and the value is trimmed. 2-200 charsAhmad Saleh
    customer_phonestringyesPalestinian mobile. Accepts 05XXXXXXXX, +9705XXXXXXXX, +9725XXXXXXXX, 009705XXXXXXXX and Arabic digits; stored normalized as 05XXXXXXXX.0599123456
    customer_phone2stringnoOptional second phone, same rule as `customer_phone`.0569123456
    customer_emailstring (email)noOptional, validated as an email address. 0-320 charsahmad@example.com
    customer_citystringyesMust match a `city_name` from `/api/v1/shipping-cities` (case and spacing insensitive) when the store configured cities. Shipping is charged from that city's cost.Ramallah
    customer_addressstringyesDelivery address; at least 5 non-space characters. 5-2000 charsAl-Masyoun, Street 5, building 12
    order_notestringnoOptional note for the shop. 0-2000 charsCall before delivery
    itemsobject[]yes1 to 50 order lines. Send ids and quantities only. 1-50 items
    items[].product_idstring (uuid)yes`Product.id`.6f1c1c1e-5a40-4e0d-9d52-1d1a0b8f3a11
    items[].quantityintegeryesUnits to buy (1-9999). 1 to 99992
    OrderCreatedReturned by `POST /api/v1/orders` (HTTP 201).

    Returned by `POST /api/v1/orders` (HTTP 201).

    FieldTypeRequiredDescriptionExample
    idstring (uuid)no6f1c1c1e-5a40-4e0d-9d52-1d1a0b8f3a11
    order_numberstringnoCustomer-facing order number. Show it on the confirmation page and use it to track the order.ORD-2026-A1B2C3
    statusOrderStatusno
    subtotalnumbernoList-price total of the lines before offers and shipping.100
    shipping_amountnumbernoShipping charged, taken from the city's cost.20
    discount_amountnumbernoPromotional saving computed by the server from active offers (never client supplied).0
    total_amountnumbernoAmount the customer pays on delivery: lines after offers + shipping. The source of truth for the confirmation page.120
    OrderReturned by `GET /api/v1/orders/{orderNumber}`.

    Returned by `GET /api/v1/orders/{orderNumber}`.

    FieldTypeRequiredDescriptionExample
    order_numberstringnoORD-2026-A1B2C3
    customer_namestringnoAhmad Saleh
    customer_phone_maskedstring | nullnoAll but the last four digits replaced by `*`.******3456
    customer_addressobject | string | nullnoDelivery address as stored (includes the city).
    subtotalnumberno100
    shipping_amountnumberno20
    tax_amountnumbernoAlways 0 for API orders.0
    discount_amountnumberno0
    total_amountnumberno120
    statusOrderStatusno
    payment_methodstringno`cash` for API orders.cash
    payment_statusPaymentStatusno
    shipping_methodstring | nullnoSet by the shop when it ships the order.
    tracking_numberstring | nullnoCarrier tracking number, filled in by the shop.
    notesstring | nullnoThe customer's order note.
    created_atstring (date-time)no2026-10-10T09:15:00.000Z
    updated_atstring (date-time)no2026-10-10T10:02:00.000Z
    itemsOrderItem[]no
    OrderItemA line of an order, snapshotted at purchase time (later price changes do not alter it).

    A line of an order, snapshotted at purchase time (later price changes do not alter it).

    FieldTypeRequiredDescriptionExample
    product_idstring | null (uuid)noNull if the product was later deleted.
    product_namestringnoVitamin C cream
    product_skustring | nullnoCRM-001
    quantityintegerno2
    unit_pricenumbernoUnit price charged (offer applied).42.5
    total_pricenumberno`unit_price` x `quantity`.85
    product_image_urlstring | null (uri)no
    OrderStatusOrder lifecycle status

    Order lifecycle status. Orders created through this API start as `pending`. The shop owner changes the status in NShop; the API never does. Stock is adjusted by the shop in NShop for now: the API and the status changes do not reserve, deduct or release stock automatically.

    one of: pending, confirmed, processing, shipped, delivered, cancelled, refunded

    PaymentStatusPayment state, maintained by the shop

    Payment state, maintained by the shop. API orders are cash on delivery and start as `pending`.

    one of: pending, paid, failed, refunded

    PageMetaPagination info returned next to `data` on list endpoints.

    Pagination info returned next to `data` on list endpoints.

    FieldTypeRequiredDescriptionExample
    totalintegeryesTotal matching rows across all pages.11
    limitintegeryesPage size used.24
    pageintegeryes1-based page number returned.1
    has_morebooleanyes`true` when another page exists.false
    ErrorEvery non-2xx response has this shape.

    Every non-2xx response has this shape.

    FieldTypeRequiredDescriptionExample
    errorobjectyes
    error.codestringyesStable machine-readable code. Branch on this, not on `message`. `insufficient_stock` is reserved for when stock enforcement is enabled and is not returned today. one of: invalid_api_key, insufficient_scope, ecommerce_disabled, rate_limited, validation_error, not_found, conflict, insufficient_stock, product_not_available, idempotency_conflict, internal_errorvalidation_error
    error.messagestringyesHuman-readable English text. May change; do not parse it.The API key is invalid, revoked or expired.
    error.detailsobjectnoExtra context, e.g. `product_id`, `required_scope` or `issues` (validation failures).{"product_id":"6f1c1c1e-5a40-4e0d-9d52-1d1a0b8f3a11"}

    Placing and tracking orders

    text
    // 1. Cities and costs (cache 1h)        GET  /api/v1/shipping-cities
    // 2. Live product data for the cart       GET  /api/v1/products?ids=<uuid>,<uuid>
    // 3. Place the order (server-side)        POST /api/v1/orders   + Idempotency-Key
    // 4. Show the confirmation                use data.order_number and data.total_amount
    // 5. Track later                          GET  /api/v1/orders/{n}?phone_last4=1234

    Send ids and quantities only. The server prices every line from live data (including active offers), takes the shipping cost from the selected city and applies no tax and no manual discount. Show the cart total as an estimate and the order response as the source of truth. Stock is not enforced yet: the API does not reject an order that exceeds stock, so the shop reviews every order in NShop. stock_quantity and in_stock are for display only.

    Request fields and limits

    FieldRuleNotes
    customer_namerequired, 2-200 charsControl characters stripped, trimmed.
    customer_phonerequired, Palestinian mobile05XXXXXXXX, +9705XXXXXXXX, +9725XXXXXXXX, 009705XXXXXXXX or Arabic digits; stored as 05XXXXXXXX.
    customer_phone2optional, same ruleSecond contact number.
    customer_emailoptional, valid email, max 320Validated as an address.
    customer_cityrequiredMust match a city_name from /shipping-cities (case and spacing insensitive) when the store has cities. Sets the shipping cost.
    customer_addressrequired, 5-2000 charsAt least 5 non-space characters.
    order_noteoptional, max 2000 charsShown to the shop.
    itemsrequired, 1-50 linesEach line: product_id (UUID) and quantity (integer 1-9999). Nothing else.
    unit_price, total_price, discount_amount, tax_amount, shipping_amount, payment_methodnot acceptedUnknown fields are rejected with 400 validation_error. The server prices everything.

    Retries. Generate one Idempotency-Key per checkout attempt (for example when the customer opens checkout) and reuse it on retries after timeouts or 502s. You will never create two orders.

    Tracking. Ask the customer for their order number and the last four digits of their phone. A wrong combination answers 404 not_found, identical to an unknown order, and the phone is returned masked.

    Order lifecycle

    status can be pending, confirmed, processing, shipped, delivered, cancelled or refunded; payment_status can be pending, paid, failed or refunded. The shop owner changes them in NShop; the API only reads them. The API creates orders as cash on delivery, so status and payment_status both start as pending and becomes paid when the shop records the payment. tracking_number and shipping_method are filled in by the shop when it ships. Poll GET /orders/{n} (no more than every few minutes) to refresh a tracking page.

    StatusMeaningStock effect
    pendingOrder received. This is the status of every new API order; payment_status is pending too.Stock is adjusted by the shop in NShop for now.
    confirmedThe shop reviewed and accepted the order.Same: handled by the shop.
    processingThe shop is preparing the order.Same: handled by the shop.
    shipped, deliveredOn its way / handed over.Same: handled by the shop.
    cancelled, refundedOrder closed without fulfilment.Same: the shop restocks manually if needed.

    When something goes wrong

    SituationWhat you getWhat to do
    Item sold out meanwhileNot rejected today (no stock enforcement); insufficient_stock is reserved for laterShow stock_quantity as a hint and refresh the cart from GET /products?ids= before checkout; the shop reviews the order.
    Product unpublished422 product_not_availableRemove the line.
    Unknown city400 validation_error (customer_city)Offer the cities from /shipping-cities.
    Timeout / 502No response or internal_errorRetry with the same Idempotency-Key.
    429rate_limited + Retry-AfterBack off, then retry.

    Keeping your catalog fresh

    NShop does not push changes to your site. Your storefront decides how fresh the catalog is, in one of two ways: cache reads with a short TTL, or keep a local copy and sync it incrementally with updated_since.

    Option 1: cache with a TTL. Fetch on demand and cache on your side (Next.js: fetch with next: { revalidate: 60, tags: ["storefront-catalog"] }). Suggested TTLs: products and categories 60-600 s, store settings, theme, shipping cities and banners 300-3600 s, orders never. To refresh sooner, call your own revalidate endpoint (for example /api/revalidate with revalidateTag) whenever you need a refresh, such as from a scheduled job. This replaces the old push from the POS.

    Option 2: incremental sync. GET /products?updated_since=<ISO-8601> returns only products whose updated_at is on or after that instant (a full ISO-8601 datetime such as 2026-09-21T06:45:26+00:00; a date-only value like 2026-09-21 returns 400 validation_error) (still name-sorted and paginated). Store the timestamp of your last successful sync, subtract a few minutes of overlap, and merge the result by product id. An invalid value returns 400 validation_error.

    Limitation. A product that was unpublished, deactivated or deleted stops appearing in listings; it is not reported as deleted. After an incremental sync, drop local products that are missing from a full listing: run a full refresh periodically (for example daily, or at night) by paging through /products without updated_since. Always re-read price (and stock_quantity, which is informational) from the API at checkout time (GET /products?ids=), never from your copy.

    typescript
    // Incremental catalog sync (run every few minutes, on your server)
    const OVERLAP_MS = 5 * 60 * 1000; // re-read a 5 minute overlap so nothing is missed
    
    async function syncCatalog(lastSyncIso: string | null) {
      const startedAt = new Date();
      const since = lastSyncIso ? new Date(new Date(lastSyncIso).getTime() - OVERLAP_MS).toISOString() : undefined;
    
      let page = 1;
      for (;;) {
        const qs = new URLSearchParams({ limit: "100", page: String(page) });
        if (since) qs.set("updated_since", since);
        const res = await fetch("https://nshop.naqsh-tech.com/api/v1/products?" + qs, {
          headers: { Authorization: "Bearer " + process.env.NSHOP_API_KEY },
        });
        if (!res.ok) throw new Error("sync failed: " + res.status);
        const { data, meta } = await res.json();
    
        for (const product of data) await db.products.upsert(product.id, product); // merge by id
        if (!meta.has_more) break;
        page += 1;
      }
      return startedAt.toISOString(); // store this as the new last-sync timestamp
    }

    Retries, timeouts and error handling

    Set a client timeout of about 10 seconds. Retry only 429, 500, 502 and network errors, with exponential backoff plus jitter (and honour Retry-After on 429). Never retry other 4xx errors: the request itself is wrong (fix it). When you retry POST /orders, send the same Idempotency-Key so the order can never be created twice. Log the X-Request-Id of failed calls.

    StatusRetry?Action
    200 / 201-Done.
    400, 404, 422NoFix the request or remove the item.
    401, 403NoCheck the key, its scopes and that e-commerce is enabled.
    409Only `conflict`conflict: retry once with the same Idempotency-Key. idempotency_conflict: you reused a key with a different body.
    429YesWait Retry-After seconds.
    500, 502, timeoutYesBackoff + jitter, same Idempotency-Key for orders.
    typescript
    async function withRetry<T>(call: () => Promise<Response>, maxAttempts = 4): Promise<Response> {
      for (let attempt = 1; ; attempt++) {
        const controller = new AbortController();
        const timer = setTimeout(() => controller.abort(), 10_000); // 10 s timeout
        try {
          const res = await call(/* pass controller.signal to fetch */);
          if (res.status !== 429 && res.status !== 500 && res.status !== 502) return res; // success or a 4xx: do not retry
          if (attempt === maxAttempts) return res;
          const retryAfter = Number(res.headers.get("Retry-After")) * 1000;
          const backoff = Math.min(8000, 500 * 2 ** attempt) * (0.5 + Math.random()); // exponential + jitter
          await new Promise((r) => setTimeout(r, retryAfter || backoff));
        } catch (err) {
          if (attempt === maxAttempts) throw err; // network error or timeout
          await new Promise((r) => setTimeout(r, 500 * 2 ** attempt * (0.5 + Math.random())));
        } finally {
          clearTimeout(timer);
        }
      }
    }

    Good to know

    Server-to-server only (CORS). CORS is not enabled on the API endpoints (only the public openapi.json file is readable from anywhere), so browsers cannot call the API directly, and they must never hold a key anyway. Call NShop from your server (route handlers, server components, a backend) and expose only the data you need to your front end.

    Money and currency. Amounts are JSON numbers with up to 2 decimals, in the shop currency (GET /store returns company.ecommerce_currency, falling back to company.currency). Do not add or multiply prices as floats: compute in integer cents or a decimal library and round only for display. Offers are already inside price; do not subtract them again. Use compare_at_price only for a crossed-out price.

    Search. q (max 100 chars) is a case-insensitive substring match on product name, SKU and barcode. It is not fuzzy and does not rank results; listings are sorted by name.

    Images. images holds public URLs, main image first, and is an empty array when a product has none. Treat them like any external image: cache them, or serve them through your image optimizer (for example next/image with the host allowed in remotePatterns), and show a placeholder when the array is empty.

    Postman and code generators. Import /api/v1/openapi.json into Postman or Insomnia, then create a collection variable apiKey and set the collection authorization to Bearer Token with {{apiKey}}. The same file works with OpenAPI Generator to produce a client in your language.

    Versioning and deprecation

    The version is in the path (/api/v1). Inside v1 we only make additive changes: new endpoints, new optional query parameters, new response fields and new enum values. Your client must ignore unknown fieldsand tolerate unknown enum values. A breaking change (removing or renaming a field, changing a type or meaning) ships as v2, and v1 keeps working for at least 6 months after v2 is announced. Every change is listed in the changelog below.

    Support

    Contact NShop on WhatsApp: 00972595856529. Always include the X-Request-Id response header of the failing call (and the time, the endpoint and the HTTP status); it lets us find the exact request. Never send your full API key: the first characters (for example nsk_live_ab12cd) are enough to identify it.

    Migrating from direct database calls

    Replace each database function with the matching API call and delete the Supabase client. Your server components keep working: only the data functions change. Keep the storefront-config (1 h) and storefront-catalog(10 min) tags on your fetch calls so your existing /api/revalidate hook still busts the cache. The full step-by-step guide is in docs/api/storefront-migration.md.

    TodayUse insteadNotes
    getServerSupabase, COMPANY_ID, SUPABASE_SERVICE_ROLE_KEYNSHOP_API_KEY + NSHOP_API_BASE_URLDelete the Supabase client and all SUPABASE_* env vars.
    getCompanyDataGET /api/v1/store → companyWhitelisted public fields only.
    getEcommerceSettingsGET /api/v1/store → settingsSame call as above.
    getEcommerceTheme(name)GET /api/v1/store → themeAlready resolved from the store's theme.
    getEcommerceShippingCitiesGET /api/v1/shipping-cities
    getEcommerceBannerStatementsGET /api/v1/banner-statements
    getEcommerceProductsGET /api/v1/productsServer-side pagination; the ~150-request assembly is gone.
    getEcommerceProductByIdGET /api/v1/products/{id}
    getEcommerceCategoryTreeGET /api/v1/categories?tree=true
    getBestSellerProductIdsGET /api/v1/best-sellersAggregated in SQL.
    getEcommerceOrderByNumberGET /api/v1/orders/{n}?phone_last4=Now requires the phone's last four digits.
    ecommercePlaceOrderRpc, POST /api/ordersPOST /api/v1/ordersSend only product_id + quantity.
    POST /api/revalidateKeep it (it is yours)Rebuild the cache with fetch tags; for fresher data use updated_since (see Keeping the catalog fresh).
    getEcommerceCategories, getEcommerceCategoryIconUrls, getStorefrontCategoryNavSkeleton, place-order.tsDeleteDead code.

    Changelog

    VersionDateChanges
    v1.1.02026-10-10GET /products accepts updated_since for incremental sync. Documentation: object reference, order lifecycle and statuses, retries, versioning policy.
    v1.0.02026-10-10Initial release: store, shipping cities, banners, categories, products, best sellers, order placement (idempotent) and tracking. Key-based authentication with scopes, rate limiting and request ids.