Allegro API Integration: Auth, Offers, Orders and Rate Limits
Allegro API integration explained: OAuth2 flows, token lifetimes, product offers, stock sync, order event polling, rate limits and the traps to avoid.
Long Nguyen
Lập trình viên Fullstack · Kỹ sư AI · Nhà nghiên cứu
What an Allegro API integration actually covers
An Allegro API integration connects your store, ERP or warehouse system to the Allegro REST API, so offers, stock, prices, orders and tracking numbers move without anyone opening the seller panel. It comes down to four jobs: authorize a seller account with OAuth2, publish offers tied to the Allegro product catalog, keep stock and prices in sync, and pull orders from an event journal while pushing fulfillment status back.
Every figure below was checked against the Allegro Developer Portal on . Allegro changes this API often, so treat the numbers as a snapshot and the structure as the durable part.
| Job | Main endpoints | What catches people |
|---|---|---|
| Authorization | /auth/oauth/authorize, /auth/oauth/token, /auth/oauth/device | Refresh tokens are single-use and rotate on every refresh |
| Offers | POST /sale/product-offers, PATCH /sale/product-offers/{offerId} | A 202 response is not a published offer |
| Stock and prices | /sale/offer-quantity-change-commands, /sale/offer-price-change-commands | One command applies one change to every offer in it |
| Orders | GET /order/events, GET /order/checkout-forms/{id} | You poll for orders, and events can arrive out of order |
| Fulfillment | PUT /order/checkout-forms/{id}/fulfillment, POST /order/checkout-forms/{id}/shipments | Stale order revisions return 409 |
Environments, headers and the Allegro sandbox
Allegro runs production and sandbox as two fully separate environments, each with its own accounts, app keys and hosts. Nothing carries over between them, so hosts and credentials belong in per-environment configuration from the first commit.
| Production | Sandbox | |
|---|---|---|
| API | https://api.allegro.pl | https://api.allegro.pl.allegrosandbox.pl |
| OAuth | https://allegro.pl/auth/oauth/ | https://allegro.pl.allegrosandbox.pl/auth/oauth/ |
| App registration | https://apps.developer.allegro.pl | https://apps.developer.allegro.pl.allegrosandbox.pl |
Four headers decide whether a request is accepted at all:
Authorization: Bearer {token}on every resource call.Accept: application/vnd.allegro.public.v1+json, plus the same value asContent-Typeon requests with a body. Plainapplication/jsonis the usual reason for a406or415on a first attempt.User-Agentin the formAppName/Version (+URL). Allegro treats it as mandatory and uses it to identify your app, so only the version part should ever change.Accept-Languageis optional:pl-PLoren-US, withcs-CZ,sk-SK,hu-HUanduk-UAon some resources.
Methods still in testing use a beta media type such as application/vnd.allegro.beta.v1+json. Allegro states that beta methods can change in ways that break compatibility, so isolate any beta call behind its own function. The full header and environment rules are in the REST API basics guide on the Allegro Developer Portal (the page opens in Polish).
Sandbox behaviour worth knowing before you test
- Category and parameter data is refreshed once a quarter, and all sandbox offers are deleted when that happens.
- Uploaded images are removed after 7 days.
- Two-step login is not required. If you turn it on, the code is always
123456. - Addresses must be real and consistent (postal code matching city and street), or account activation can fail.
- Rate limits are the same as in production.
The quarterly wipe is the reason to write a seed script on day one. A team that builds its test catalog by hand rebuilds it by hand every three months.
Allegro API authentication: choosing an OAuth2 flow
The app type is chosen when you register the app and cannot be changed afterwards, so this decision comes before any code.
| Flow | Use it when | Notes |
|---|---|---|
| Authorization Code (PKCE recommended) | Your app has a browser step: a web app, or a SaaS serving many sellers | Seller logs in on Allegro and consents; you receive a one-time code. PKCE uses S256 and a 43 to 128 character verifier |
| Device flow | No browser on the machine: a console job, a warehouse service, a cron worker | Seller types a user_code at a verification URL while your app polls the token endpoint |
| Client credentials | You only need public data such as the category tree | No seller consent, no access to seller resources |
| Dynamic Client Registration | You ship software that each customer installs on their own infrastructure | Each installation gets its own credentials; requires a Software Statement ID from Allegro |
For a single seller wiring their own ERP to Allegro, the device flow is usually the least work: no public redirect URL to host. For a product serving many sellers, use Authorization Code with PKCE.
Token lifetimes and limits
| Item | Value |
|---|---|
| Access token lifetime | 12 hours |
| Refresh token lifetime | 3 months |
| Refresh token reuse | Single-use. Each refresh returns a new pair; the old refresh token keeps working for 60 seconds |
| Active sessions per user | 20 |
| App keys per account | 5 |
| Token endpoint method | POST only, parameters in the body (GET stopped working on ) |
Refresh token rotation is where most broken Allegro integrations fail, and it fails late. Two workers refresh at the same moment, each receives a different new refresh token, one overwrites the other in storage, and the integration dies at the next refresh with nobody watching. Refresh under a lock, re-read the stored token inside the lock, and save both new tokens before anything else uses them:
import requests
TOKEN_URL = 'https://allegro.pl/auth/oauth/token'
def refresh_tokens(store, client_id, client_secret, redirect_uri=None):
# store.lock() must be a real cross-process lock: a DB row lock or a Redis lock
with store.lock('allegro-token'):
current = store.load() # re-read inside the lock
if not current.expires_soon():
return current # another worker already refreshed
data = {
'grant_type': 'refresh_token',
'refresh_token': current.refresh_token,
}
if redirect_uri: # authorization code apps
data['redirect_uri'] = redirect_uri
resp = requests.post(
TOKEN_URL,
auth=(client_id, client_secret),
data=data, # body only, never the query string
timeout=30,
)
resp.raise_for_status()
fresh = resp.json()
# Save BOTH tokens now. The old refresh token dies 60 seconds after this call.
return store.save(
access_token=fresh['access_token'],
refresh_token=fresh['refresh_token'],
expires_in=fresh['expires_in'],
)Three more rules that save a support ticket each:
- Refresh on a schedule, not only on demand. A seasonal seller whose integration sits idle for three months comes back to a dead refresh token and has to authorize again by hand.
- Reuse the access token for its full 12 hours. Allegro tells you to reuse it until it expires, and excess token requests for one user return
429. - Expect tokens to be revoked from outside. A password or email change, a logout from all devices, or the seller removing your app invalidates both tokens. Detect the
401and send the seller back through authorization instead of retrying forever.
Scopes follow the pattern allegro:api:orders:read, allegro:api:sale:offers:write, allegro:api:shipments:write. Declare only what the integration uses. A stock-sync tool has no business holding allegro:api:payments:write.
How to list products through the Allegro API
Offers on Allegro are attached to products in a shared catalog. You create an offer with POST /sale/product-offers and point it at a catalog product, either by the product UUID or by a GTIN or MPN. If the product already exists, a short request is enough:
import requests
API = 'https://api.allegro.pl'
MEDIA = 'application/vnd.allegro.public.v1+json'
headers = {
'Authorization': f'Bearer {access_token}',
'Accept': MEDIA,
'Content-Type': MEDIA, # requests keeps this when you pass json=
'User-Agent': 'AcmeSync/1.4.0 (+https://example.com/acmesync)',
}
offer = {
'productSet': [
{'product': {'id': '5901234123457', 'idType': 'GTIN'}},
],
'external': {'id': 'SKU-10432'}, # your own SKU, filterable later
'sellingMode': {'price': {'amount': '129.00', 'currency': 'PLN'}},
'stock': {'available': 25, 'unit': 'UNIT'},
}
resp = requests.post(f'{API}/sale/product-offers', json=offer, headers=headers, timeout=30)
if resp.status_code == 202:
operation_url = resp.headers['Location'] # poll this, not the offer
wait_seconds = int(resp.headers.get('Retry-After', 5))The response code tells you what happened:
201 Created: the offer exists and is visible.202 Accepted: the request is structurally valid and queued for further validation. It can still be rejected. The response carries aLocationheader andRetry-After: 5.422: a value failed validation. Theerrorsarray names the field inpath.
After a 202, poll GET /sale/product-offers/{offerId}/operations/{operationId} and not the offer. Polling the offer hides validation errors. Allegro says most offers clear within minutes, and a brand new catalog product can take up to two hours in rare cases, so the job that publishes offers must not be the job that waits for them.
Five traps in offer creation
- POST is not idempotent. Retrying a timed-out request creates a second offer. Send your SKU in
external.id, and before any retry checkGET /sale/offers?external.id=SKU-10432. - Shipping and returns default by name. If the account has several shipping rate tables and none is called
default, an offer without an explicit table returns422. Business accounts hit the same rule for return and warranty policies. Either name one of eachdefaultor always send the IDs. - Catalog conflicts come back as
PARAMETER_MISMATCH. When your product data disagrees with the catalog entry, the422includes the matchingproductIdinmetadata. Link to that product, or file a change proposal withPOST /sale/products/{productId}/change-proposals. - Defaults are filled in only on POST. On
PATCH, completing required parameters is your job. - Arrays are replaced whole. Adding one image through
PATCHmeans resending every existing image URL. An offer takes 1 to 16 images, supplied as URLs.
Syncing stock and prices at scale
Allegro offers four ways to change stock or price. Picking the wrong one for your catalog size is how an integration ends up fighting the rate limit.
| Method | Scope | Best for |
|---|---|---|
PATCH /sale/product-offers/{offerId} | One offer. A stock-only or price-only change skips validation of the rest of the offer | Event-driven updates: one SKU sold in your store, one offer adjusted |
PUT /sale/offer-quantity-change-commands/{commandId} | Many offers, one change: FIXED or GAIN | Setting a group of offers to the same quantity, for example zero during a stocktake |
PUT /sale/offer-price-change-commands/{commandId} | Up to 1,000 offers per call, one change: fixed, increase, decrease, or percentage | Campaign pricing and across-the-board adjustments |
POST /sale/offer-bulk-modification-commands (beta) | Up to 25 modifications per request, price and stock together | Different values per offer in one request |
The command endpoints are asynchronous. You generate the commandId yourself as a fresh UUID, then read the result from the command and its /tasks report, where each offer is marked SUCCESS or FAIL:
import uuid
command_id = str(uuid.uuid4())
body = {
'modification': {'changeType': 'FIXED', 'value': 0},
'offerCriteria': [{
'type': 'CONTAINS_OFFERS',
'offers': [{'id': '17600000001'}, {'id': '17600000002'}],
}],
}
requests.put(
f'{API}/sale/offer-quantity-change-commands/{command_id}',
json=body, headers=headers, timeout=30,
)
# later: GET /sale/offer-quantity-change-commands/{command_id}/tasksNotice the shape: one modification for every offer listed. A nightly sync where each SKU has its own quantity does not fit a quantity command. That workload is per-offer PATCH calls driven by what actually changed, never a full-catalog push.
To see changes Allegro made on its side, read GET /sale/offer-events. It reports OFFER_STOCK_CHANGED (also fired after a purchase), OFFER_PRICE_CHANGED, OFFER_ENDED and others, but only for the last 24 hours. An integration that is down for a day and a half cannot replay what it missed and needs a full reconciliation against GET /sale/offers.
Once Allegro shares stock with other channels, the difficult question stops being which endpoint to call and becomes which system owns the stock number and how fast every channel hears about a sale. That design work, across Amazon, eBay, Walmart, Etsy and similar APIs, is what our marketplace integration service is for.
Getting orders: poll the Allegro event journal
Allegro delivers orders through a journal you poll. The order documentation describes no push notification for new orders: you call GET /order/events, remember the last event ID you processed, and ask for everything after it. Coming from Amazon SP-API or eBay, where the platform can notify you, this is the main design difference to plan for.
| Event | Meaning | What to do |
|---|---|---|
BOUGHT | Purchase made, not paid | Reserve stock if you want; do not ship |
FILLED_IN | Buyer filled in the delivery form. Can repeat, and the data can still change | Nothing final yet |
READY_FOR_PROCESSING | Payment finished, or cash on delivery or pickup chosen. Address and invoice data are final | Create the order in your system |
BUYER_CANCELLED | Buyer cancelled, before or after the order was ready | Cancel and release stock |
AUTO_CANCELLED | Allegro cancelled the order automatically | Cancel and release stock |
FULFILLMENT_STATUS_CHANGED | The seller changed the fulfillment status | Usually your own echo; ignore or log |
def poll_order_events(access_token, cursor, on_ready, on_cancelled):
params = {'limit': 1000}
last_id = cursor.get() # None on the very first run
if last_id:
params['from'] = last_id
resp = requests.get(
f'{API}/order/events',
headers={
'Authorization': f'Bearer {access_token}',
'Accept': 'application/vnd.allegro.public.v1+json',
'User-Agent': 'AcmeSync/1.4.0 (+https://example.com/acmesync)',
},
params=params,
timeout=30,
)
resp.raise_for_status()
for event in resp.json()['events']:
order_id = event['order']['checkoutForm']['id']
if event['type'] == 'READY_FOR_PROCESSING':
on_ready(order_id) # fetch /order/checkout-forms/{id} inside
elif event['type'] in ('BUYER_CANCELLED', 'AUTO_CANCELLED'):
on_cancelled(order_id)
cursor.set(event['id']) # advance only after the handler succeededFive rules make this loop safe in production:
- Decide where the first run starts. Without
from, the journal returns the 100 oldest events it holds, and it holds 60 days. To start from now, read the latest event ID fromGET /order/event-statsand use it as the first cursor. - Make handlers idempotent and key them on
checkoutForm.id. Allegro warns that events can repeat and arrive out of order. - Always fetch the order. The event tells you something changed;
GET /order/checkout-forms/{id}tells you the current truth. - Watch for merged purchases. If one
lineItem.idshows up in two orders, the buyer combined them and the oldercheckoutForm.idis removed. - Do not use the order list as your feed.
GET /order/checkout-formsreturns at most 100 orders per call,offsetpluslimitcannot exceed 10,000, and it covers 12 months. It is for backfills and reconciliation.
Sending status and tracking back
Update the status with PUT /order/checkout-forms/{id}/fulfillment, using values such as PROCESSING, READY_FOR_SHIPMENT, SENT or PICKED_UP. Send the order revision you last read with the update. If the order changed in the meantime, Allegro answers 409: fetch the order again and retry.
Add tracking with POST /order/checkout-forms/{id}/shipments, passing a carrierId from GET /order/carriers and the waybill. Leave out lineItems and the number applies to the whole order.
Move the status to PROCESSING as soon as you start picking. For business-account sellers, the documentation describes a buyer cancellation window of 3 days from purchase that applies while the fulfillment status is unchanged and no tracking number has been added. An order left on NEW in your queue can be cancelled after you have packed it.
Allegro API rate limits and error handling
| Limit | Value | When exceeded |
|---|---|---|
| Global | 9,000 requests per minute per Client ID | 429, and the Client ID is blocked for one minute |
| Selected resources (leaky bucket) | Counted per user | Responses slow down first; too many parallel requests for one user return 429 |
| Token requests | Excess requests for one user | 429 from the token endpoint |
| Individual resources | Lower limits, stated per endpoint in the reference | Varies |
The global limit is counted per Client ID and not per seller. For one seller, 9,000 a minute is generous. For a product serving two hundred sellers through one app, it is a shared budget, and one seller running a full-catalog resync can lock out everyone for a minute. Put a single rate limiter in front of all outgoing calls and give each seller a slice.
Resource errors arrive as an errors array. Each item has a code, a developer-facing message, a userMessage you can show the seller, and a path for validation errors. Log the Trace-Id response header with every failure, because Allegro support asks for it.
| Status | Usual cause | Right response |
|---|---|---|
401 | Expired or revoked token | Refresh once; if that fails, ask the seller to authorize again |
403 | No rights to the resource, for example a sales block on the account | Do not retry; surface it to the seller |
406 / 415 | Wrong Accept or Content-Type | Fix the media type |
409 | Stale order revision | Fetch the order again, then retry |
422 | Validation failure | Do not retry; show userMessage |
429 | Rate limit | Stop all calls for that Client ID for a minute |
503 | Allegro unavailable | Retry with backoff, except for non-idempotent POSTs |
Selling on Allegro.cz, .sk and .hu from one integration
Allegro runs marketplaces in Poland, Czechia, Slovakia and Hungary, and one integration reaches all of them. There is no second API to connect.
- Offers: an offer has a base marketplace and an
additionalMarketplacesobject keyed by marketplace ID (allegro-cz,allegro-sk,allegro-hu,allegro-business-cz). Each entry can carry its own price in that market's currency, for example CZK onallegro-czwhile the base offer is in PLN. - Orders: every order carries
marketplace.id, andGET /order/checkout-formsfilters on it. - Content:
Accept-Languageselects the language on resources that support it.
The trap is in your own data model. If your orders table assumes PLN and Polish VAT, the first Czech order breaks invoicing. Store currency and marketplace on every order and every price from the start, even if you launch in Poland only.
Maintenance: the changelog is part of the integration
An Allegro integration is never finished. Allegro publishes changes on its developer news feed, and the notice period is often measured in weeks. These are the items on the feed in early October 2026, plus the 2025 authorization change that still breaks older code:
| Effective | Change | Who it breaks |
|---|---|---|
| Token endpoint stopped accepting GET; parameters must be in the POST body | Older OAuth code that put parameters in the URL | |
For business accounts, purchase issues become a Message Center type handled only through /messaging | Customer service tools | |
Ship with Allegro returns tracking numbers in a transportingInfo list instead of additionalProperties | Label and tracking code | |
Seller ratings public.v1 switches to the structure currently served by beta.v1 | Anything parsing ratings | |
| Turnover discount resources are removed from the public API | Pricing and promotion tools |
The Message Center move to beta.v1 on /messaging was announced on with roughly three weeks of notice. That is the realistic pace. Someone has to read the Allegro Developer Portal news feed every week, and each announcement needs an owner and a date. Budget for it as a running cost and not as a surprise.
Two habits make these changes cheap: keep every Allegro call behind one client module so a response change is a one-file fix, and run a nightly contract test against the sandbox that fails loudly when a field you depend on disappears.
Custom Allegro integration or a ready-made connector?
Not every seller needs custom code. A fair way to decide:
| Your situation | Better fit | Why |
|---|---|---|
| Standard store platform, a few hundred SKUs, no unusual rules | Ready-made multichannel connector | The vendor absorbs the changelog for you |
| Custom ERP, in-house warehouse system, or a store platform no connector supports | Custom integration | There is nothing for a connector to plug into |
| Pricing rules per market, bundles, multi-warehouse stock allocation | Custom integration | Connector rule engines run out before your business logic does |
| Large catalog with frequent stock movement | Custom, or a connector you have load-tested | Sync delay turns directly into overselling |
| You sell software to other Allegro sellers | Custom, with Authorization Code or Dynamic Client Registration | You are the connector |
If you build, the order that keeps risk low is: authorization with safe token storage, then read-only order polling running in parallel with your current process, then fulfillment and tracking, then stock sync, and offer creation last. Offer creation has the most validation rules and the least urgency, since listings can be created in the seller panel while everything else is automated.
If you would rather hand this over, or you are unsure whether a connector already covers your case, send us the details through the free quote form. Tell us the systems involved and your catalog size, and we will reply with a scope and a price, including when the honest answer is that a connector is enough.
CÂU HỎI THƯỜNG GẶP
Câu hỏi thường gặp
Does the Allegro API have webhooks for new orders?
Allegro's order documentation describes a pull model. You poll GET /order/events, store the ID of the last event you processed, and pass it as the from parameter on the next call. The journal keeps 60 days of events and returns up to 1,000 per request.
What is the Allegro API rate limit?
The main limit is 9,000 requests per minute per Client ID, in production and in the sandbox. Going over it returns HTTP 429 and blocks the Client ID for one minute. Some resources have lower limits listed in their reference entries, and selected resources apply a per-user leaky bucket.
How long do Allegro API tokens last?
An access token lasts 12 hours and a refresh token lasts 3 months. Refresh tokens are single-use: each refresh returns a new access token and a new refresh token, and the old refresh token stops working 60 seconds later.
Which OAuth2 flow should I use for an Allegro integration?
Use Authorization Code with PKCE for web apps and products serving many sellers, the device flow for server jobs and console tools with no browser, and client credentials only for public data such as categories. The app type is fixed at registration and cannot be changed later.
Is there an Allegro API sandbox?
Yes. The sandbox is a separate environment at api.allegro.pl.allegrosandbox.pl with its own accounts and app keys. Its rate limits match production, uploaded images are removed after 7 days, and all sandbox offers are deleted once a quarter when category data is refreshed.
Can one Allegro API integration sell on Allegro.cz, Allegro.sk and Allegro.hu?
Yes. An offer can list extra markets in additionalMarketplaces, keyed by IDs such as allegro-cz, allegro-sk and allegro-hu, each with its own price in the local currency. Orders carry a marketplace.id field so you can tell the markets apart.