Online Shop

How to connect a custom shop to easySales with the API

How to connect a custom shop to easySales with the API

Connect any custom or unsupported shop platform to easySales through the API: the two credentials you need, pushing products and stock, sending orders, and getting changes back.

When you need this

easySales has ready-made connectors for the common shop platforms. If you run something custom — an in-house storefront, or a platform we do not have an integration for — you can connect it yourself through the API.

This is a developer task. You will write code that pushes your catalogue into easySales and sends orders across, and easySales will call your shop back when something changes. Nothing here needs a plugin installed.

How the connection works

Two credentials are involved, and confusing them is the most common reason a first integration returns 403.

Credential Where it comes from How it is sent If it is missing
Access token POST /oauth/token, using the OAuth client you create in API Settings Authorization: Bearer <token> header 401
Website token The connect wizard's Configuration step A website_token field in the request body, not a header 403 Invalid website token
The two credentials, and what each one is for

Both are needed on every write. The access token proves who is calling; the website token says which shop the data belongs to.

Step 1 — Add the website

Go to Integrations → Websites, press Connect a Website, fill the form, then choose the API tile and the API V2 version below it.

The Connect a Website form in easySales with the API tile selected and a green check, and the API V2 version tile shown below under the heading Choose the platform version your site uses.
Pick the API tile, then API V2 underneath it.
Field What it does
Website Name Your own label. It is how you tell your shops apart later.
Website URL Your storefront address, with the right protocol.
Country Sets which VAT rates the next field offers. Permanent.
Price Vat The VAT applied to products that arrive without one. Chosen from a list, not typed.
Shipping Vat The VAT applied to delivery when an order does not carry one.
Language / Currency The shop's language and the currency your prices are in. Both permanent.
New order custom status Optional. Lands every imported order in one of your own statuses.
Inventory source Where the authoritative stock number lives. Defaults to easySales.
Implicit package type The default used when generating AWBs for these orders.
Invoice Series The billing series used when your payload does not supply one.
The connect form

Step 2 — Copy the website token

The wizard's Configuration step shows a 60-character token. This is the one the article you may have read before never mentions, and the one every write call needs.

The Configuration step of the easySales connect wizard for API V2, showing the read-only 60-character generated website token with a copy-to-clipboard button.
The website token. Every write call carries this, in the body.

Copy it with the button rather than retyping it, and store it as a secret in your application. Press Save to create the website.

The shop now appears under Integrations → Websites, carrying the same token on its card — that is where to find it again later.

The Websites screen in easySales showing a connected API V2 shop card with its URL, price VAT, website token and Active toggle.
A connected API shop. The token on the card is the same one your code sends.

Step 3 — Create an OAuth client

Go to Settings → API Settings. This is where the OAuth credentials live.

The API Settings screen in easySales, listing an OAuth client with its client id, name and masked secret, and a personal access token below it.
Settings → API Settings. OAuth clients above, personal access tokens below.

Press Create new client, name it after the system that will be calling, and set Grant type to Website Grant.

The Create client form in easySales API Settings with the name filled in and Grant type set to Website Grant, showing the explanation that the Website Grant obtains an access token using a website_token.
Website Grant — the one that trades your website token for an access token.

Website Grant is the one built for this job: it exchanges your website token for an access token, with no browser redirect and no user interaction. Save, and copy the Client ID and Secret that appear.

Step 4 — Get an access token

POST https://easy-sales.com/oauth/token
Content-Type: application/json

{
  "grant_type":    "website",
  "client_id":     "your-client-id",
  "client_secret": "your-client-secret",
  "website_token": "your-60-character-website-token",
  "scope":         "add-products update-products update-stock add-orders update-orders read-orders"
}

The reply is a standard OAuth body:

{
  "token_type":    "Bearer",
  "expires_in":    31536000,
  "access_token":  "eyJ0eXAiOiJKV1Qi...",
  "refresh_token": "def50200a1b2c3..."
}

Send it as Authorization: Bearer <access_token> on every subsequent call.

Step 5 — Push your catalogue

The base URL for everything below is https://easy-sales.com/api/v2.

Order matters. Categories and characteristics have to exist before a product can reference them, or the product call is rejected.

  1. POST /categories/save — your categories
  2. POST /characteristics/save — your attributes
  3. POST /products/save — your products
POST https://easy-sales.com/api/v2/products/save
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "website_token": "your-60-character-website-token",
  "product": {
    "product_website_id": "10231",
    "sku": "TS-BLK-M",
    "name": "Black T-shirt, size M",
    "sale_price": 79.99,
    "full_price": 99.99,
    "stock": 24,
    "type": "simple",
    "tax_rate": 21,
    "url": "https://my-shop.example.com/p/ts-blk-m",
    "images": ["https://my-shop.example.com/img/ts-blk-m.jpg"],
    "categories": ["cat-778"],
    "characteristics": [{ "id": "char-12", "value": "Black" }]
  }
}
Field Required Notes
product_website_id Yes Your shop's own id for the product
sku Yes Unique per website; the upsert key
name Yes
sale_price Yes The price the customer pays
stock Yes Must be a number
type Yes simple or complex
full_price No Pre-discount price, if you show one
tax_rate No Must be a rate valid for the website's country
categories No Your own category ids — each must already exist
characteristics No Your own attribute ids — each must already exist
images No Array of URLs we can fetch
weight, handling_time, description, url No Used for couriers, delivery estimates and marketplace listings
Product fields

Step 6 — Keep stock in sync

Stock is the thing that has to stay current, and it is the cheapest call to make often. Send up to 100 rows at a time:

PATCH https://easy-sales.com/api/v2/stocks/bulk
Authorization: Bearer <access_token>

{
  "website_token": "your-60-character-website-token",
  "data": [
    { "sku": "TS-BLK-M", "stock": 24 },
    { "sku": "TS-BLK-L", "stock": 0 }
  ]
}

Every SKU must already exist on this website; unknown ones are rejected rather than created.

Step 7 — Send your orders in

POST https://easy-sales.com/api/v2/orders/save
Authorization: Bearer <access_token>

{
  "website_token": "your-60-character-website-token",
  "order": {
    "order_id": "SO-100244",
    "order_date": "2026-09-09 14:31:00",
    "order_total": 179.98,
    "status": "new",
    "payment_mode": "card",
    "shipment_tax": 21,
    "billing_address":  { "country": "RO", "city": "Cluj-Napoca", "street": "Str. Memorandumului 4" },
    "shipping_address": { "country": "RO", "city": "Cluj-Napoca", "street": "Str. Memorandumului 4" },
    "order_products": [
      { "product_website_id": "10231", "sku": "TS-BLK-M", "name": "Black T-shirt, size M",
        "quantity": 2, "price": 79.99, "total": 159.98 }
    ]
  }
}

Post the same order_id again and the order is updated rather than duplicated, which makes retries safe.

Step 8 — Get changes back out

Your shop needs to know when an order is invoiced, has an AWB, or changes status. Subscribe a webhook rather than polling:

GET https://easy-sales.com/api/v2/webhooks
Authorization: Bearer <access_token>
POST https://easy-sales.com/api/v2/webhooks/order-updated
Authorization: Bearer <access_token>

{
  "url":    "https://my-shop.example.com/hooks/easysales",
  "secret": "a-shared-secret-of-at-least-24-characters"
}

You subscribe to one event at a time, so repeat the call for each of order-created, order-updated, awb-created, invoice-created and delivery-status-updated.

Each delivery is signed, so verify the signature before trusting the body. Answer quickly — the request times out after a few seconds and is retried a limited number of times, so acknowledge first and process afterwards.

Limits

Limit Value
Requests 500 per minute across the whole API
Token requests 60 per minute — cache your token
Products, stock, prices, categories, characteristics per bulk call 100
Products per bulk archive call 50
Websites per account 30
Product description 65,534 characters
Limits worth designing around

When something fails

Code Meaning Usual cause
400 Invalid json The body is not valid JSON
401 Unauthenticated Missing, expired or malformed access token
403 Invalid website token website_token missing or wrong
403 The website has been deactivated The website's Active switch is off
422 Validation failed A required field is missing, a SKU is duplicated, or a category or characteristic does not exist yet
429 Too many requests Over 500 per minute
503 Temporarily unavailable Ingestion is backed up; retry after the interval given
What each response code means

Anything that fails on our side also lands in Products → Online Shops → Website errors, with the payload and a Retry action, which is usually faster than reproducing the call.

Reference

The full endpoint reference, with every field and every response, is published at https://api.easy-sales.com.

Frequently asked questions

The access token is an OAuth2 bearer token that identifies your account, and it goes in the Authorization header. The website token identifies one specific shop, and it goes in the request body as website_token. Write calls need both: 401 means the access token is wrong, 403 with "Invalid website token" means the website token is.

There is no refresh rate for a custom shop, and no field for one on the form. Nothing is polled — you decide when to push products, stock and orders, and you receive changes back through webhooks. The old article documented a field that is no longer part of the connect flow.

No. API websites are not connection-tested, because there is nothing for easySales to call until you start sending data. The badge is set when the record is created. The real test is whether your products appear under Products → Online Shops → Products.

Website Grant. It is built for exactly this: it exchanges the website token for an access token with no redirect and no user interaction, which is what a server-to-server integration needs. Personal Access Tokens work too, but they are account-wide rather than shop-scoped and are shown only once.

Access tokens, refresh tokens and personal access tokens are all valid for a year. The website token has no expiry at all — it lives until you press Refresh Token or delete the website. Cache your access token rather than requesting a new one per call; the token endpoint is rate-limited to 60 requests a minute.

Categories and characteristics must exist before a product can reference them. Push categories first, then characteristics, then products. The same applies to bulk stock updates: every SKU must already exist on the website.

Yes, if you use the /save endpoints. POST /products/save upserts on SKU and POST /orders/save upserts on order_id, so retries are safe and repeat deliveries do not create duplicates. The plain POST /products and POST /orders endpoints reject duplicates with a 422 instead.

Not for a custom shop. Stock travels one way, from your shop into easySales, so your shop stays the source of truth. What does come back is order-level: status changes, AWBs and invoices, delivered through the webhooks you subscribe to.

Was this guide helpful?