Marketplace Selling

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.

Photo de profil de Long Nguyen

Long Nguyen

Développeur fullstack · Ingénieur IA · Chercheur

• • 4 min de lecture •

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.

JobMain endpointsWhat catches people
Authorization/auth/oauth/authorize, /auth/oauth/token, /auth/oauth/deviceRefresh tokens are single-use and rotate on every refresh
OffersPOST /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-commandsOne command applies one change to every offer in it
OrdersGET /order/events, GET /order/checkout-forms/{id}You poll for orders, and events can arrive out of order
FulfillmentPUT /order/checkout-forms/{id}/fulfillment, POST /order/checkout-forms/{id}/shipmentsStale 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.

ProductionSandbox
APIhttps://api.allegro.plhttps://api.allegro.pl.allegrosandbox.pl
OAuthhttps://allegro.pl/auth/oauth/https://allegro.pl.allegrosandbox.pl/auth/oauth/
App registrationhttps://apps.developer.allegro.plhttps://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 as Content-Type on requests with a body. Plain application/json is the usual reason for a 406 or 415 on a first attempt.
  • User-Agent in the form AppName/Version (+URL). Allegro treats it as mandatory and uses it to identify your app, so only the version part should ever change.
  • Accept-Language is optional: pl-PL or en-US, with cs-CZ, sk-SK, hu-HU and uk-UA on 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.

FlowUse it whenNotes
Authorization Code (PKCE recommended)Your app has a browser step: a web app, or a SaaS serving many sellersSeller logs in on Allegro and consents; you receive a one-time code. PKCE uses S256 and a 43 to 128 character verifier
Device flowNo browser on the machine: a console job, a warehouse service, a cron workerSeller types a user_code at a verification URL while your app polls the token endpoint
Client credentialsYou only need public data such as the category treeNo seller consent, no access to seller resources
Dynamic Client RegistrationYou ship software that each customer installs on their own infrastructureEach 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

ItemValue
Access token lifetime12 hours
Refresh token lifetime3 months
Refresh token reuseSingle-use. Each refresh returns a new pair; the old refresh token keeps working for 60 seconds
Active sessions per user20
App keys per account5
Token endpoint methodPOST 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 401 and 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 a Location header and Retry-After: 5.
  • 422: a value failed validation. The errors array names the field in path.

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

  1. POST is not idempotent. Retrying a timed-out request creates a second offer. Send your SKU in external.id, and before any retry check GET /sale/offers?external.id=SKU-10432.
  2. 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 returns 422. Business accounts hit the same rule for return and warranty policies. Either name one of each default or always send the IDs.
  3. Catalog conflicts come back as PARAMETER_MISMATCH. When your product data disagrees with the catalog entry, the 422 includes the matching productId in metadata. Link to that product, or file a change proposal with POST /sale/products/{productId}/change-proposals.
  4. Defaults are filled in only on POST. On PATCH, completing required parameters is your job.
  5. Arrays are replaced whole. Adding one image through PATCH means 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.

MethodScopeBest for
PATCH /sale/product-offers/{offerId}One offer. A stock-only or price-only change skips validation of the rest of the offerEvent-driven updates: one SKU sold in your store, one offer adjusted
PUT /sale/offer-quantity-change-commands/{commandId}Many offers, one change: FIXED or GAINSetting 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 percentageCampaign pricing and across-the-board adjustments
POST /sale/offer-bulk-modification-commands (beta)Up to 25 modifications per request, price and stock togetherDifferent 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}/tasks

Notice 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.

EventMeaningWhat to do
BOUGHTPurchase made, not paidReserve stock if you want; do not ship
FILLED_INBuyer filled in the delivery form. Can repeat, and the data can still changeNothing final yet
READY_FOR_PROCESSINGPayment finished, or cash on delivery or pickup chosen. Address and invoice data are finalCreate the order in your system
BUYER_CANCELLEDBuyer cancelled, before or after the order was readyCancel and release stock
AUTO_CANCELLEDAllegro cancelled the order automaticallyCancel and release stock
FULFILLMENT_STATUS_CHANGEDThe seller changed the fulfillment statusUsually 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 succeeded

Five 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 from GET /order/event-stats and 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.id shows up in two orders, the buyer combined them and the older checkoutForm.id is removed.
  • Do not use the order list as your feed. GET /order/checkout-forms returns at most 100 orders per call, offset plus limit cannot 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

LimitValueWhen exceeded
Global9,000 requests per minute per Client ID429, and the Client ID is blocked for one minute
Selected resources (leaky bucket)Counted per userResponses slow down first; too many parallel requests for one user return 429
Token requestsExcess requests for one user429 from the token endpoint
Individual resourcesLower limits, stated per endpoint in the referenceVaries

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.

StatusUsual causeRight response
401Expired or revoked tokenRefresh once; if that fails, ask the seller to authorize again
403No rights to the resource, for example a sales block on the accountDo not retry; surface it to the seller
406 / 415Wrong Accept or Content-TypeFix the media type
409Stale order revisionFetch the order again, then retry
422Validation failureDo not retry; show userMessage
429Rate limitStop all calls for that Client ID for a minute
503Allegro unavailableRetry 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 additionalMarketplaces object 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 on allegro-cz while the base offer is in PLN.
  • Orders: every order carries marketplace.id, and GET /order/checkout-forms filters on it.
  • Content: Accept-Language selects 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:

EffectiveChangeWho it breaks
Token endpoint stopped accepting GET; parameters must be in the POST bodyOlder OAuth code that put parameters in the URL
For business accounts, purchase issues become a Message Center type handled only through /messagingCustomer service tools
Ship with Allegro returns tracking numbers in a transportingInfo list instead of additionalPropertiesLabel and tracking code
Seller ratings public.v1 switches to the structure currently served by beta.v1Anything parsing ratings
Turnover discount resources are removed from the public APIPricing 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 situationBetter fitWhy
Standard store platform, a few hundred SKUs, no unusual rulesReady-made multichannel connectorThe vendor absorbs the changelog for you
Custom ERP, in-house warehouse system, or a store platform no connector supportsCustom integrationThere is nothing for a connector to plug into
Pricing rules per market, bundles, multi-warehouse stock allocationCustom integrationConnector rule engines run out before your business logic does
Large catalog with frequent stock movementCustom, or a connector you have load-testedSync delay turns directly into overselling
You sell software to other Allegro sellersCustom, with Authorization Code or Dynamic Client RegistrationYou 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.

FAQ

Questions fréquentes

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.

Restez informé avec Netalith

Recevez des ressources de développement, des mises à jour produit et des offres spéciales directement dans votre boîte mail.