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 |
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.
| 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. |
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.
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.
Step 3 — Create an OAuth client
Go to Settings → API Settings. This is where the OAuth credentials live.
Press Create new client, name it after the system that will be calling, and set Grant type to Website Grant.
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.
POST /categories/save— your categoriesPOST /characteristics/save— your attributesPOST /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 |
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 |
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 |
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.