# التغييرات والتراجع عنها

أغلب كتابات الإعدادات التي تمر عبر الواجهة البرمجية تُسجَّل كتغييرات: إعدادات المتجر وتصميمه وقالبه، وأقسام الصفحة الرئيسية، والفئات، والمخزون، وأكواد الخصم، والبيكسلات، وأسعار الشحن وإعدادات التوصيل، وأقسام صفحات الهبوط. يحتفظ كل تغيير بالقيم التي استبدلها، والتراجع يعيد كتابتها عبر الفحوص نفسها التي مرّت بها الكتابة الأصلية. الكتابات التي يُجريها على المتجر Copilot أو مساعد ذكاء اصطناعي متصل (Claude أو ChatGPT) أو تطبيق مثبّت تُسجَّل هي أيضاً. أما التغييرات المحفوظة من لوحة التحكم فلا تُسجَّل.

النقاط الثلاث أدناه تعرض قائمة التغييرات، وتقرأ تغييراً واحداً كاملاً، وتتراجع عن تغيير. الكتابات تحت `/v1/store/home-layout` ومزامنة الأسعار من شركة التوصيل تُرجع `change_id`. أما بقية الكتابات فابحث عن تغييرها عبر `GET /v1/changes`.

## ما الذي يُسجَّل[​](#ما-الذي-يُسجَّل "رابط مباشر إلى ما الذي يُسجَّل")

| `entity`              | تُسجّله                                                                                      | `entity_id`                                              | الصلاحية لقراءته كاملاً | الصلاحية للتراجع عنه  |
| --------------------- | -------------------------------------------------------------------------------------------- | -------------------------------------------------------- | ----------------------- | --------------------- |
| `store.settings`      | `PATCH /v1/store`                                                                            | `settings`                                               | `store:read`            | `store:write`         |
| `store.design`        | `PATCH /v1/store/design`                                                                     | `design`                                                 | `store:read`            | `store:write`         |
| `store.theme`         | `POST /v1/store/theme` و`POST /v1/store/fast-checkout-theme` و`POST /v1/store/variant-style` | `theme`                                                  | `store:read`            | `store:write`         |
| `store.home_sections` | `PATCH /v1/store/home-sections`                                                              | `home_sections`                                          | `store:read`            | `store:write`         |
| `store.home_layout`   | كل كتابة تحت `/v1/store/home-layout`                                                         | `layout`                                                 | `store:read`            | `store:write`         |
| `category`            | `POST /v1/categories` و`PATCH` و`DELETE /v1/categories/{id}`                                 | رقم الفئة                                                | `products:read`         | `products:write`      |
| `stock`               | `POST /v1/products/{id}/stock`                                                               | رقم المنتج                                               | `products:read`         | `products:write`      |
| `promo_code`          | `POST /v1/promo-codes` و`PATCH` و`DELETE /v1/promo-codes/{id}`                               | رقم كود الخصم                                            | `promos:read`           | `promos:write`        |
| `pixels`              | `POST /v1/pixels` و`PATCH` و`DELETE /v1/pixels/{id}`                                         | رقم البيكسل في DZBuild، وليس `pixel_id`                  | `pixels:read`           | `pixels:write`        |
| `shipping.rates`      | `POST /v1/shipping/rates` و`POST /v1/shipping/rates/sync`                                    | `rates`                                                  | `shipping:read`         | `shipping:write`      |
| `shipping.settings`   | `PATCH /v1/shipping/settings`                                                                | `settings`                                               | `shipping:read`         | `shipping:write`      |
| `lp.section`          | كتابات الأقسام تحت `/v1/landing-pages/{id}/sections`                                         | رقم القسم، أو `lp:` متبوعة برقم الصفحة عند إعادة الترتيب | `landing_pages:read`    | `landing_pages:write` |

* هذه الكتابات لا تُسجَّل ولا يمكن التراجع عنها: المنتجات مع صورها ومتغيراتها وعروضها وإضافاتها وقواعد الكمية (المخزون يُسجَّل)، والطلبات، وصفحات الهبوط نفسها، وترتيب الفئات، وشركات التوصيل، والـ `webhooks`، والمفاتيح.
* القيمة `lp.page` مقبولة كفلتر لـ `entity`، لكن لا توجد كتابة تُسجّلها.
* التسجيل لا يوقف الكتابة. إذا تعذّر تسجيل تغيير، مثلاً لأن قيمه قبل الكتابة أو بعدها تتجاوز 256 KB، تتم الكتابة رغم ذلك ولا يمكن التراجع عنها. كتابات أسعار الشحن هي الاستثناء: تفحص الحجم أولاً وتُرجع `422 snapshot_too_large` بدل أن تُنفَّذ.
* كل صلاحية في الجدول ضمن الصلاحيات الافتراضية للمفتاح الجديد، فالمفتاح المُنشأ من لوحة التحكم (**الإعدادات ← واجهة API**، `/dashboard/api`) يستطيع استعمال النقاط الثلاث. الصلاحيات تُجمَّد لحظة إنشاء المفتاح، فالمفتاح القديم الذي تنقصه الصلاحية المطلوبة يُرجع `403 forbidden`: أنشئ مفتاحاً جديداً من لوحة التحكم.

## `GET /v1/changes`[​](#get-v1changes "رابط مباشر إلى get-v1changes")

تغييرات المتجر، الأحدث أولاً، دون القيم التي استبدلتها.

**المصادقة:** مفتاح منصة بصلاحية `store:read`. رمز التطبيق المثبّت يتلقى `403 forbidden` (`Apps cannot use this endpoint`).

### معاملات الاستعلام[​](#معاملات-الاستعلام "رابط مباشر إلى معاملات الاستعلام")

| المعامل  | النوع  | الافتراضي | ملاحظات                                                                                                 |
| -------- | ------ | --------- | ------------------------------------------------------------------------------------------------------- |
| `entity` | string | لا شيء    | تغييرات `entity` واحدة من الجدول أعلاه فقط. أي قيمة أخرى تُتجاهل وتعود القائمة كاملة.                   |
| `limit`  | int    | 25        | من 1 إلى 100. القيمة الأصغر تُحسب 1، والأكبر تُحسب 100.                                                 |
| `cursor` | string | لا شيء    | قيمة `next_cursor` من الصفحة السابقة. انظر [الترقيم](https://dzbuild.com/ar/ar/api-docs/pagination.md). |

### الطلب[​](#الطلب "رابط مباشر إلى الطلب")

```
curl 'https://api.dzbuild.app/v1/changes?limit=2' \

  -H "Authorization: Bearer $DZ_KEY"
```

### الاستجابة 200[​](#الاستجابة-200 "رابط مباشر إلى الاستجابة 200")

```
{

  "data": {

    "items": [

      {

        "id": 120,

        "entity": "shipping.rates",

        "entity_id": "rates",

        "action": "update",

        "summary": "Shipping rates updated for 2 wilaya(s)",

        "undone_at": null,

        "created_at": "2026-10-06 11:02:17",

        "undone": false

      },

      {

        "id": 119,

        "entity": "promo_code",

        "entity_id": "7",

        "action": "update",

        "summary": "Updated promo code SUMMER10",

        "undone_at": null,

        "created_at": "2026-10-06 10:52:30",

        "undone": false

      }

    ],

    "next_cursor": "MTE5",

    "has_more": true

  }

}
```

| الحقل        | المعنى                                                                                      |
| ------------ | ------------------------------------------------------------------------------------------- |
| `id`         | رقم التغيير، لـ `GET /v1/changes/{id}` وللتراجع.                                            |
| `entity`     | ما الذي تغيّر. انظر الجدول أعلاه.                                                           |
| `entity_id`  | العنصر الذي تغيّر، كنص. انظر الجدول أعلاه.                                                  |
| `action`     | `create` أو `update` أو `delete`. التراجع يُسجَّل كـ `update`.                              |
| `summary`    | وصف قصير بالإنجليزية. التراجع يُكتب `Undo of change #` متبوعاً برقم التغيير الذي تراجع عنه. |
| `undone`     | `true` بعد التراجع عن التغيير.                                                              |
| `undone_at`  | وقت التراجع عن التغيير، وإلا `null`.                                                        |
| `created_at` | `YYYY-MM-DD HH:MM:SS` بتوقيت الخادم. و`undone_at` بالصيغة نفسها.                            |

## `GET /v1/changes/{id}`[​](#get-v1changesid "رابط مباشر إلى get-v1changesid")

تغيير واحد مع `before`، أي القيم التي استبدلها، و`after`، أي القيم التي كتبها. شكلهما يتبع نوع العنصر: بعضها يحتفظ فقط بالحقول التي لمستها الكتابة، وبعضها بالعنصر كله.

**المصادقة:** مفتاح منصة بصلاحية `store:read`، إضافة إلى صلاحية القراءة الخاصة بنوع التغيير في الجدول أعلاه. رمز التطبيق المثبّت يتلقى `403 forbidden`.

### الطلب[​](#الطلب-1 "رابط مباشر إلى الطلب")

```
curl 'https://api.dzbuild.app/v1/changes/118' \

  -H "Authorization: Bearer $DZ_KEY"
```

### الاستجابة 200[​](#الاستجابة-200-1 "رابط مباشر إلى الاستجابة 200")

```
{

  "data": {

    "id": 118,

    "key_id": "dzpk_live_xxxxxxxxxxxxxx",

    "entity": "category",

    "entity_id": "12",

    "action": "update",

    "summary": "Updated category #12 (name, slug)",

    "undone_at": null,

    "undone_by_id": null,

    "created_at": "2026-10-06 10:41:05",

    "before": {"name": "Shoes", "slug": "shoes"},

    "after": {"name": "Sneakers", "slug": "sneakers"},

    "undone": false

  }

}
```

تحمل الاستجابة حقول القائمة، وهذه الحقول أيضاً.

| الحقل          | المعنى                                                                                |
| -------------- | ------------------------------------------------------------------------------------- |
| `key_id`       | المفتاح الذي أجرى التغيير، أو `null` لتراجع أُجري من لوحة التحكم.                     |
| `undone_by_id` | رقم التغيير الذي تراجع عن هذا التغيير، وإلا `null`.                                   |
| `before`       | القيم التي استبدلها التغيير. `null` في حالة `create`.                                 |
| `after`        | القيم التي كتبها التغيير. `null` في حالة `delete` وفي مزامنة الأسعار من شركة التوصيل. |

رمز الوصول (`access token`) الخاص بالبيكسل لا يُحفظ أبداً في التغيير. إذا كان للبيكسل رمز، يظهر `••••••••` مكانه في `before` و`after`.

### الأخطاء[​](#الأخطاء "رابط مباشر إلى الأخطاء")

| HTTP | الرمز         | السبب                                                                                                                    |
| ---- | ------------- | ------------------------------------------------------------------------------------------------------------------------ |
| 400  | `bad_request` | رقم التغيير في المسار ليس أرقاماً فقط.                                                                                   |
| 403  | `forbidden`   | المفتاح لا يملك `store:read` أو صلاحية القراءة الخاصة بنوع التغيير (`Missing scope: ...`)، أو النداء من رمز تطبيق مثبّت. |
| 404  | `not_found`   | لا يوجد تغيير بهذا الرقم في المتجر.                                                                                      |

## `POST /v1/changes/{id}/undo`[​](#post-v1changesidundo "رابط مباشر إلى post-v1changesidundo")

يعيد كتابة قيم `before` الخاصة بالتغيير عبر الفحوص نفسها التي مرّت بها الكتابة الأصلية، ثم يسجّل التراجع كتغيير جديد. بلا جسم للطلب.

**المصادقة:** مفتاح منصة بصلاحية التراجع الخاصة بنوع التغيير في الجدول أعلاه، ولا حاجة إلى `store:read`. **يتطلب `Idempotency-Key`.** رمز التطبيق المثبّت يتلقى `403 forbidden` (`Apps cannot use this endpoint`). يرى مالك المتجر آخر 20 تغييراً أجراها كل تطبيق مثبّت في صفحة ذلك التطبيق في لوحة التحكم (`/dashboard/apps/{id}`، قسم **آخر التغييرات التي أجراها**)، ويستطيع التراجع عنها من هناك بزر **تراجع**.

### ما يفعله التراجع[​](#ما-يفعله-التراجع "رابط مباشر إلى ما يفعله التراجع")

1. التغيير الذي أنشأ شيئاً ليس له ما يُستعاد، فيُرجع `422 nothing_to_restore`: احذف العنصر بدل ذلك. إضافة قسم للصفحة الرئيسية عبر `/v1/store/home-layout` هي الاستثناء، لأن كل كتابة على تخطيط الصفحة الرئيسية تُسجَّل كـ `update` للتخطيط كله.
2. التراجع عن `update` يعيد كتابة قيم `before` فوق ما هو موجود الآن. الصفحة الرئيسية وحدها تفحص التغييرات اللاحقة وتُرجع `409 layout_changed` (انظر [أقسام الصفحة الرئيسية](https://dzbuild.com/ar/ar/api-docs/resources/home-layout.md)). للتراجع عن عدة تغييرات على العنصر نفسه، ابدأ بالأحدث.
3. الفئة المحذوفة تعود برقمها، دون صورتها. وكود الخصم المحذوف يعود برقمه إذا لم يأخذه كود آخر، ومع عدد مرات استعماله.
4. البيكسل المحذوف يعود برقمه إذا بقي متاحاً، لكن دون رمز الوصول ودون ربطه بالمنتجات والفئات وصفحات الهبوط. والتراجع عن تعديل بيكسل يترك رمز الوصول الحالي كما هو.
5. قسم صفحة الهبوط المحذوف يعود برقم جديد.
6. التراجع عن المخزون يعيد كل قيمة إلى الرقم المسجَّل قبل التغيير، مهما فعلت الطلبات بالمخزون منذ ذلك الحين.
7. التراجع عن `POST /v1/shipping/rates` يعيد الأسعار السابقة، ويحذف سعر الولاية التي لم يكن لها سعر قبل التغيير. والتراجع عن مزامنة الأسعار من شركة التوصيل يعيد كل سعر استبدلته المزامنة، ويُبقي الأسعار التي أضافتها لولايات لم يكن لها سعر.
8. التراجع يُسجَّل تغييراً مستقلاً، `undo_change_id`، ويمكنك التراجع عنه لتطبيق التغيير الأصلي من جديد. إذا لم يكن للتغيير الأصلي `after` (حذف أو مزامنة أسعار من شركة التوصيل)، يُرجع التراجع عن التراجع `422 nothing_to_restore`.
9. يمكن التراجع عن التغيير مرة واحدة. أي تراجع لاحق يُرجع `409 already_undone`، ومن تراجعَين يُرسَلان في اللحظة نفسها لا يُنفَّذ إلا واحد.
10. إذا فشلت الاستعادة نفسها (`restore_target_missing`، أو قيمة مرفوضة، أو `500`)، لا يُعلَّم التغيير كمتراجَع عنه، فيمكنك إعادة المحاولة بعد إصلاح السبب. قواعد إعادة المحاولة أسفله.

استجابة التراجع لا تحمل القيم المستعادة. اقرأ العنصر من جديد. عبر `api.dzbuild.app`، قد تُرجع ذاكرة القراءة المؤقتة القصيرة (`GET /v1/store` وكل طلب `GET` تحته، وقائمتا `GET /v1/products` و`GET /v1/landing-pages`) القيمَ القديمة لمدة تصل إلى 30 ثانية بعد التراجع.

### الطلب[​](#الطلب-2 "رابط مباشر إلى الطلب")

```
curl -X POST 'https://api.dzbuild.app/v1/changes/118/undo' \

  -H "Authorization: Bearer $DZ_KEY" \

  -H "Idempotency-Key: undo-118"
```

### الاستجابة 200[​](#الاستجابة-200-2 "رابط مباشر إلى الاستجابة 200")

```
{

  "data": {

    "undone": true,

    "change_id": 118,

    "entity": "category",

    "undo_change_id": 121

  }

}
```

| الحقل            | المعنى                                                      |
| ---------------- | ----------------------------------------------------------- |
| `undone`         | دائماً `true` مع `200`.                                     |
| `change_id`      | التغيير الذي تم التراجع عنه.                                |
| `entity`         | نوعه.                                                       |
| `undo_change_id` | التغيير الذي يسجّل هذا التراجع، أو `null` إذا تعذّر تسجيله. |

### الأخطاء[​](#الأخطاء-1 "رابط مباشر إلى الأخطاء")

| HTTP | الرمز                    | السبب                                                                                                    |
| ---- | ------------------------ | -------------------------------------------------------------------------------------------------------- |
| 400  | `bad_request`            | رقم التغيير في المسار ليس أرقاماً فقط، أو `Idempotency-Key` غائب أو بصيغة خاطئة.                         |
| 403  | `forbidden`              | المفتاح لا يملك صلاحية التراجع الخاصة بنوع التغيير (`Missing scope: ...`)، أو النداء من رمز تطبيق مثبّت. |
| 404  | `not_found`              | لا يوجد تغيير بهذا الرقم في المتجر.                                                                      |
| 409  | `already_undone`         | سبق التراجع عن التغيير.                                                                                  |
| 409  | `layout_changed`         | الصفحة الرئيسية فقط: تغيّر التخطيط بعد هذا التغيير.                                                      |
| 422  | `not_undoable`           | هذا النوع من التغييرات لا يمكن التراجع عنه.                                                              |
| 422  | `nothing_to_restore`     | التغيير أنشأ شيئاً، أو لا يحمل قيماً تُستعاد. احذف العنصر بدل ذلك.                                       |
| 422  | `restore_target_missing` | ما لمسه التغيير لم يعد موجوداً، مثلاً فئة أو قسم صفحة هبوط حُذف منذ ذلك الحين.                           |
| 422  | `idempotency_key_reuse`  | استُعمل `Idempotency-Key` نفسه من قبل لطلب مختلف، مثل التراجع عن تغيير آخر.                              |
| 4xx  | رمز الكتابة الأصلية      | فحوص الكتابة الأصلية ترفض القيم، مثلاً `invalid_wilaya` في أسعار الشحن، أو قالب لم يعد متاحاً.           |
| 500  | `server_error`           | تعذّر تنفيذ التراجع. يبقى التغيير قابلاً للتراجع عنه.                                                    |

### إعادة المحاولة و`Idempotency-Key`[​](#إعادة-المحاولة-وidempotency-key "رابط مباشر إلى إعادة-المحاولة-وidempotency-key")

أول استجابة لكل مفتاح تُحفظ 24 ساعة، بما فيها استجابات `4xx`. إعادة المحاولة بالمفتاح نفسه للتغيير نفسه تُرجع تلك الاستجابة مع `Idempotency-Replay: 1`، ولا يُنفَّذ تراجع ثانٍ. لذلك بعد إصلاح سبب الخطأ، أعد المحاولة بمفتاح جديد. استجابات `5xx` و`429` لا تُحفظ أبداً، فأعد المحاولة بالمفتاح نفسه. انظر [Idempotency](https://dzbuild.com/ar/ar/api-docs/idempotency.md).
