Payments are experiencing issues due to temporary restrictions in Russia. If your payment does not go through, please submit a support request.Our support team is available 24/7 — we are always here to help with hosting and server issues.We are now accepting requests for dedicated server rental and colocation services in our data center.Reminder: we recommend enabling backups for additional data protection.A new VPS/VDS lineup with NVMe storage and improved performance is now available.Maintenance work on some servers has been completed. All services are operating normally.
Article9 min readViews0

API Delta: How to Prevent Double-Counting of Product Quantities

On the tested model, we compare the change in product quantity against a record of its absolute value. A single operation_id prevents the same command from being applied twice, while version checking rejects stale snapshots after new product movements.

Comments 0

A purple container with packages and a yellow package stopped at a receiving barrier in a warehouse
In this article

If a command means 'add 3,' a network retry must arrive with the same operation_id, and the receiver must return the original result without changing the product unit count a second time. If a command means 'set to 8,' an exact retry is inherently safer, but a stale snapshot can overwrite a newer sale. Therefore, delta and absolute values must not be mixed in a single implicit field.

In a small example, the initial product quantity is 5 units. The first shipment adds 3 units. Without protection, two identical requests yield 11. With an operation log, the first request records 8, and the second receives a duplicate result, leaving the quantity at 8 units. The network sometimes repeats a request—not out of stubbornness, but simply because it did not receive a response.

API Request

Exact Repetition

Separate Risk

adjust +3

Without operation_id, it adds 3 again

One delivery document counted twice

set 8

Again sets 8

An old snapshot may overwrite a newer change

Why HTTP Methods Are Insufficient

According to RFC 9110, an operation is called idempotent if multiple identical requests have the same intended effect as a single one. PUT is defined as an idempotent method, but the business meaning of the field remains important: the body "delta: 3" does not become an absolute quantity of product units solely because of the chosen verb.

RFC 5789 explicitly states that PATCH is not idempotent by itself, although a request can be designed to be idempotent. For changes from a known version, the specification recommends a conditional request; for HTTP, this can be done using If-Match with a strong ETag.

Conditions for the test acceptance

Check performed on October 31, 2026, in CPython 3.12. The model operates solely in memory: it does not open network connections, write files, or connect to a database. This is a test of the mechanism, not an evaluation of a specific 1C-Bitrix edition or 1C configuration.

  • one SKU: SKU-BLUE-M; initial product quantity is 5 units, version 1;

  • adjust adds the delta to the product quantity, set assigns an absolute value;

  • operation_id is unique for the business operation, not for the network attempt;

  • A precise match returns the saved result; the same ID with a different body is rejected;

  • The receiver compares the request's expected_version with the current product version. If the versions differ, it rejects the stale set command and retains the current product quantity.

Model code

from __future__ import annotations from dataclasses import dataclass @dataclass(frozen=True) class Update: operation_id: str sku: str mode: str value: int expected_version: int | None = None class Conflict(Exception): pass class Catalog: def __init__(self) -> None: self.stock = {"SKU-BLUE-M": 5} self.version = {"SKU-BLUE-M": 1} self.applied: dict[str, tuple[Update, tuple[str, int, int]]] = {} def naive_adjust(self, sku: str, delta: int) -> int: self.stock[sku] += delta return self.stock[sku] def apply(self, update: Update) -> tuple[str, int, int]: previous = self.applied.get(update.operation_id) if previous is not None: previous_update, previous_result = previous if previous_update != update: raise Conflict("operation_id_reused_with_different_payload") return ("duplicate", previous_result[1], previous_result[2]) if update.sku not in self.stock: raise KeyError("unknown_sku") current_version = self.version[update.sku] if update.expected_version is not None and update.expected_version != current_version: raise Conflict("version_conflict") if update.mode == "adjust": self.stock[update.sku] += update.value elif update.mode == "set": if update.value < 0: raise ValueError("negative_stock") self.stock[update.sku] = update.value else: raise ValueError("unknown_mode") self.version[update.sku] += 1 result = ("applied", self.stock[update.sku], self.version[update.sku]) self.applied[update.operation_id] = (update, result) return result def show(label: str, result: object) -> None: print(f"{label}: {result}") naive = Catalog() show("naive first +3", naive.naive_adjust("SKU-BLUE-M", 3)) show("naive retry +3", naive.naive_adjust("SKU-BLUE-M", 3)) assert naive.stock["SKU-BLUE-M"] == 11 safe = Catalog() delta = Update("receipt-42", "SKU-BLUE-M", "adjust", 3) show("safe first +3", safe.apply(delta)) show("safe retry +3", safe.apply(delta)) assert safe.stock["SKU-BLUE-M"] == 8 assert safe.version["SKU-BLUE-M"] == 2 try: safe.apply(Update("receipt-42", "SKU-BLUE-M", "adjust", 4)) except Conflict as error: show("same id, changed payload", error) else: raise AssertionError("changed payload must be rejected") # A later sale changes the authoritative value after the receipt was applied. safe.stock["SKU-BLUE-M"] -= 1 safe.version["SKU-BLUE-M"] += 1 show("after later sale", (safe.stock["SKU-BLUE-M"], safe.version["SKU-BLUE-M"])) try: safe.apply(Update("snapshot-43", "SKU-BLUE-M", "set", 6, expected_version=2)) except Conflict as error: show("stale absolute value", error) else: raise AssertionError("stale absolute value must be rejected") snapshot = Update("snapshot-44", "SKU-BLUE-M", "set", 6, expected_version=3) show("current absolute value", safe.apply(snapshot)) show("current absolute retry", safe.apply(snapshot)) assert safe.stock["SKU-BLUE-M"] == 6 assert safe.version["SKU-BLUE-M"] == 4

Run result

naive first +3: 8 naive retry +3: 11 safe first +3: ('applied', 8, 2) safe retry +3: ('duplicate', 8, 2) same id, changed payload: operation_id_reused_with_different_payload after later sale: (7, 3) stale absolute value: version_conflict current absolute value: ('applied', 6, 4) current absolute retry: ('duplicate', 6, 4)

Check

Expected

Received

Naive +3 twice

Show repeat error

11 instead of 8

Same operation_id and body

Do not change the quantity of items

duplicate, 8, version 2

Same ID, different delta

Reject conflict

operation_id_reused_with_different_payload

Outdated set 6

Do not overwrite the sale

version_conflict, 7 remaining

Current set 6 and retry

Apply once

6, version 4; then duplicate

All assertions passed; the process returned exit code 0. This significant edge case is not cosmetic: after delivery, the model decrements one unit by sale, increments the version from 2 to 3, and only then retrieves the old snapshot. Without a version check, such a set could silently overwrite a newer state.

How to structure the integration contract

  • Fix the direction: 1C → site, site → 1C, or two-way sync. For each field, there must be a single source of truth for the final value in a specific operation.

  • Transmit a stable product or product variant identifier. SKU-BLUE-M in the model is a sample string; in a project, verify the actual field, product variant, and matching rules.

  • Clearly separate commands: adjust with a delta and set with an absolute value. A quantity field without a mode forces the receiver to guess, and guessing does not scale well with assortment.

  • Generate operation_id at the level of a delivery document, adjustment, or other business event. Re-delivering the same event preserves the ID; a new operation receives a new one.

  • Store the fingerprint of significant fields and the result together with the ID. An ID match with a different body indicates a conflict. Applying a new delta in this case is not allowed.

  • Changing the product quantity and recording the operation_id must be atomic. Otherwise, a failure between the two writes leaves a window: the product quantity is already changed, but there is no proof of processing yet.

  • For set, pass the expected version or use the If-Match condition if the API supports ETag. After a conflict, you must re-read the state and make a decision, not blindly repeat the old snapshot.

Before selecting a mode, it is useful to separately define the source of the final stock. This issue is discussed in the article «Stock in 1C and on the site do not match: where the final number should live». Here we solve a narrower task: how to avoid applying one already selected command twice.

Here lies a catch: the key retention period

If operation_id is deleted before duplicates disappear from the queue, the old request becomes new again. There is no universal retention period: it must exceed the maximum message age, the client's duplicate window, and the manual return window from the queue. Retention period, cleanup logic, and journal volume should be included in the integration contract, not left as a "we will figure it out later" setting.

The operations journal does not replace permission checks. A technical user must have access only to the required catalog, warehouse, and operation. Entity names, API availability, and the permission model depend on the 1C-Bitrix edition and version, the shop module, and the specific exchange method; these must be verified against the project before implementation.

Acceptance Criteria

  1. The first adjust +3 changes the stock from 5 to 8 and saves the operation_id with the result.

  2. An exact network retry returns the same business result and does not create a second movement.

  3. A retry with the same ID but a different delta is rejected as a conflict and enters observable diagnostics.

  4. After a new movement, the outdated set does not overwrite the stock; the client receives a version conflict.

  5. An unknown SKU, insufficient permissions, and a storage rejection produce an explicit error rather than a partially applied operation.

  6. Metrics distinguish between applied, duplicate, and conflict states so that safe retries do not mask actual discrepancies.

To perform acceptance testing for a specific integration, repeat these scenarios on an isolated staging environment with the actual 1C-Bitrix edition, the relevant module, exchange direction, warehouses, product variants, and technical user permissions. The training model demonstrates algorithm behavior but does not simulate a client deployment — fortunately, it has no procurement department.

Discussion 0

Share your experience and ask questions. Comments without links appear after editorial review.

No comments yet. Start the discussion.