aiomoysklad
An async toolkit for integrations with the MoySklad JSON API 1.2: a client that keeps to MoySklad's limits, plus helpers for stock, customer orders and webhooks. Русская версия: README.ru.md.
Status: 0.1.0, not released yet. Everything is tested against mocked HTTP only. It has not been tested against a live MoySklad account yet. Every limit, header and error code comes from the official docs; docs/design.md cites them and lists what only a live account can answer.
Why another client
The official generated SDK is synchronous, has no rate limiting and takes filter as a raw
string. An existing async client defaults to 45 requests per 3 seconds, which has been wrong
for user tokens since May 2026. aiomoysklad is async (httpx) and counts what MoySklad counts:
- Request weight. The basket holds 45 units per 3 seconds, counted per user and per
solution. A solution's request costs 1 unit; a draft or in-moderation solution's 4; a
user's (login and password, or a user token) 3 from 2026-09-01 and 4 from 2026-12-01. The
full stock reports cost 5 units; the short
/currentones are charged 5 too, as a precaution. - At most 5 requests in flight per user or solution (20 per account).
- 429 retries that wait for
X-Lognex-Retry-AfterorX-Lognex-Reset, and slow down early fromX-RateLimit-Remaining. APOSTis resent only when it carries asyncId. - A cut-off guard. MoySklad turns API access off after more than 200 identical failing
requests a minute, 200 answers of 429, or 100
PUTs to one entity. The client stops at 150, 150 and 75. Accept-Encoding: gzip, which is mandatory, on every request.
Install
Not on PyPI yet. From a checkout:
pip install .
Python 3.10 to 3.13; the dependencies are httpx and pydantic 2.
Start
from aiomoysklad import AuthKind, BearerToken, MoySklad
async def main(token: str) -> None:
async with MoySklad(BearerToken(token, kind=AuthKind.SOLUTION)) as ms:
async for product in ms.iter_rows("/entity/product", limit=100):
print(product["name"])
The token's kind sets each request's weight; BearerToken defaults to AuthKind.USER. Until
2026-12-01 a draft or in-moderation solution costs more than a user (4 units against 3), so
such a token left on the default is undercounted and runs into 429s. BasicAuth(login, password) is the other option. Clients with the same credentials can share one RateLimiter
and ConcurrencyLimit.
Filter builds filter= strings with the nine documented operators and escapes ;:
from aiomoysklad import Filter
f = Filter().eq("state.name", "Новый", "Принят").contains("name", "a;b")
assert str(f) == r"state.name=Новый;state.name=Принят;name~a\;b"
Stock and assortment
The assortment comes from the new endpoint without stock (the legacy one is being phased out in 2027); the amounts come from the short report, paired by id:
from aiomoysklad import join_stock
rows = [row async for row in ms.assortment.iter(group_by="variant")]
stock = await ms.stock.all_current("quantity", include_zero=True)
for row, quantity in join_stock(rows, stock, default=0.0):
print(row["name"], quantity)
ms.stock.bystore_current() groups by store as well. Rows are typed (StockRow,
StoreStockRow). StockPoller polls with changedSince and an overlap, and reads a full
report at least daily, as the docs advise.
Customer orders
from datetime import datetime
from aiomoysklad import OrderCursor
paid = await ms.customer_orders.find_state("Оплачен")
# First run: a naive time is Moscow time.
cursor = OrderCursor.from_dict(saved) if saved else OrderCursor(datetime(2026, 11, 1))
orders = ms.customer_orders.iter_updated_since(cursor)
async for order in orders:
state = order.get("state", {}).get("meta", {}).get("href")
# A PUT changes updated: next run the order comes again, already moved.
if order["payedSum"] >= order["sum"] > 0 and state != paid.meta.href:
await ms.customer_orders.set_state(order["id"], paid)
saved = orders.cursor.as_dict() # store it for the next run
order = await ms.customer_orders.create({"organization": org, "agent": agent})
The iterator pages by updated and id instead of offset, so orders changed during the walk
are not skipped. create adds a syncId, so a retried request returns the order created the
first time.
Webhooks
Webhooks work on paid plans and in the trial only. ensure creates a webhook or turns a
disabled one back on, so it is safe to run on every deploy:
from aiomoysklad import WebhookReceiver
url = "https://hooks.example.com/moysklad/a-long-secret"
await ms.webhooks.ensure("customerorder", "CREATE", url)
receiver = WebhookReceiver(ms)
for received in await receiver.receive(body, request_id, fetch=True):
print(received.event, received.entity)
MoySklad expects 200 or 204 within 1500 ms and resends with the same requestId. In an
endpoint, call receiver.accept(body, request_id) (parse and dedupe only), answer 204, and
fetch the entities afterwards. Nothing in the docs signs a webhook, so use a secret path; the
receiver fetches entities only from the API host. The in-memory dedupe store can be swapped
for anything with an atomic first_seen(key, ttl), such as Redis.
MoySklad disables webhooks whose receiver failed on more than seven of the last ten days and
handled none successfully in those ten days.
await ms.webhooks.health(url_prefix) lists the disabled ones and can re-enable them.
Errors
Each HTTP status has its own exception (NotFoundError, RateLimitError and so on) under
MoySkladError, with the parsed errors array and codes.
GuardTrippedError means the client refused to send a request that could get access cut off.
Examples
stock_sync.py (stock), sync_orders.py (orders) and webhooks.py (setup, an ASGI receiver, a health check).
Not covered
No full data model (entities are dicts), no positions sub-resources, no async tasks, no file downloads, no legacy assortment endpoint. Changes: CHANGELOG.md.
License
MIT, see LICENSE.
Metadata
Release files for aiomoysklad 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| aiomoysklad-0.1.0.tar.gz | 125.2 kB | With provenance | Details for aiomoysklad-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| aiomoysklad-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for aiomoysklad-0.1.0-py3-none-any.whl |
Total release size: 192.4 kB
Release files / aiomoysklad-0.1.0.tar.gz
| Download URL | aiomoysklad-0.1.0.tar.gz |
|---|---|
| Size | 125.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
80f14750536890d6c78cefb0706670a86940d2d2249006ecf2699f0b22e95195
|
|
BLAKE2b-256 checksum How to use checksums |
b1041914f8342856beda8ad018cf5be332d2e1eccdbb27cbd628eddff5db1f32
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.
Transparency logRelease files / aiomoysklad-0.1.0-py3-none-any.whl
| Download URL | aiomoysklad-0.1.0-py3-none-any.whl |
|---|---|
| Size | 67.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
91bdbb08c2947f2bdd169b1b74131b005478ccb37b4053a4fbf9c35a6f528b93
|
|
BLAKE2b-256 checksum How to use checksums |
6b175954490c6c258ea1ef9e6a8c2e551e68af14ef4586f548c083b575dfb558
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.
Transparency logSecurity for aiomoysklad 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for aiomoysklad-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.