A REST API for the modified eCommerce Shopsoftware - giving external applications and integrations programmatic access to shop data and operations.
The modified API exposes the core entities of a modified shop (customers, products, orders, categories, β¦) as a versioned REST interface built on top of the existing shop core.
The project focuses on:
- Secure, token-based access to shop data
- Full CRUD coverage for the most relevant shop resources
- Per-customer access control, managed directly in the shop backend
- Self-documenting endpoints via an auto-generated OpenAPI specification
The API lets merchants and developers connect modified to external systems - ERPs, marketplaces, apps, custom storefronts - without touching the shop's admin UI.
- π JWT-based authentication with short-lived access tokens
- π§© Resource-oriented endpoints for customers, products, categories, orders, manufacturers, attributes, tags, coupons, campaigns, shipping, countries, contents, newsletters, currencies, languages, configurations and DHL
- π Signed webhooks that push shop events (e.g. new orders) to external systems, with automatic retries
- π Auto-generated OpenAPI/Swagger documentation with an interactive Swagger UI (
/api/v1/docs/) - π§± Built on Slim 4 (PSR-7/PSR-15) with dependency injection
- π€ Per-customer, per-endpoint access management via the shop backend
- π¦ JSON request/response format throughout
- PHP
- Slim 4 (PSR-7 / PSR-15) + PHP-DI
- MySQL / MariaDB
- JWT (Firebase JWT / Tuupola middleware), HS256
- OpenAPI 3 (zircote/swagger-php)
- modified eCommerce Shopsoftware 3.2.0 or higher
- PHP 8.2 or higher
The minimum shop version is enforced at runtime and reported by the /v1/version endpoint. Both the API version and the minimum shop version are defined in one place in the API (version / min_shop_version in config/settings.php).
Enable API access for a customer in the shop backend (Customers β API Access), then pick either way:
In the browser (Swagger UI)
- Open
/api/v1/docs/in your browser. - Click Authorize, enter the customer's username (email) and password.
- Try any endpoint directly from the UI - the token is attached automatically.
From the command line (cURL)
# 1. Get a token (valid for 10 minutes)
curl -X POST https://your-shop.tld/api/v1/oauth \
-H "username: api@example.com" \
-H "password: your-password"
# β {"access_token":"<JWT>","token_type":"Bearer","expires":1735000000}
# 2. Call an endpoint with the token
curl https://your-shop.tld/api/v1/manufacturers \
-H "Authorization: Bearer <JWT>"Replace
https://your-shop.tldwith your shop URL. If the shop runs in a subdirectory, prefix the paths accordingly (e.g.https://your-shop.tld/shop/api/v1/...).
The API uses api/v1/.htaccess to route endpoint paths such as
/api/v1/version through its index.php front controller. On hosting setups
where Apache cannot derive the public URL path from the physical directory
(for example, some IONOS configurations), requests may reach the shop's main
404 page instead of the API.
In that case, uncomment RewriteBase in api/v1/.htaccess and set it to the
public URL path of the API:
# Shop installed at the domain root
RewriteBase /api/v1/For a shop installed in a subdirectory, include that directory as well:
# Shop available below https://your-shop.tld/shop/
RewriteBase /shop/api/v1/This setting is hosting- and installation-specific and should remain commented out when Apache resolves the relative rewrite target correctly without it.
Access is granted per customer account in the shop backend (Customers β API Access), then used in two steps:
- Get a token -
POST /v1/oauthwith the credentials of a customer that has API access enabled. Credentials may be sent either as request headers (user/username+password) or as form fields (username+password, e.g. an OAuth2 password grant). Returns a short-lived JWT access token (10 minutes) plus a long-lived refresh token (30 days):{ "access_token": "<JWT>", "token_type": "Bearer", "expires": 1735000000, "refresh_token": "<opaque-token>", "refresh_expires": 1737592000 } - Call the API - send the access token on every subsequent request:
Authorization: Bearer <JWT> - Refresh the token - when the access token is about to expire, exchange the refresh token for a new pair via
POST /v1/oauth/refresh(no credentials needed). The refresh token may be sent as a request header (refresh_token) or as a form field (refresh_token):The response has the same shape ascurl -X POST https://your-shop.tld/api/v1/oauth/refresh \ -H "refresh_token: <opaque-token>"/v1/oauth. Refresh tokens are rotated: each refresh invalidates the presented token and returns a new one, so always store the latestrefresh_token. Tokens are stored only as a hash in the shop database and can be revoked; a refresh is rejected if the account no longer exists or its API access was removed. - Log out - revoke a refresh token via
POST /v1/oauth/logout(the token itself is the proof of possession, so no credentials are needed). Add theallflag to revoke every refresh token of the account (log out on all devices):The call is idempotent and returns# revoke this session curl -X POST https://your-shop.tld/api/v1/oauth/logout \ -H "refresh_token: <opaque-token>" # revoke all sessions of the account curl -X POST https://your-shop.tld/api/v1/oauth/logout \ -H "refresh_token: <opaque-token>" -H "all: true"
{"success": true}whenever a token is supplied. The access token is stateless and simply expires on its own after 10 minutes.
In the interactive docs you can skip the manual steps: click Authorize, enter the customer's username and password, and Swagger UI fetches the token and attaches it to every request automatically.
HTTPS is required in production; only localhost/127.0.0.1 are allowed over plain HTTP.
All routes are served under /api/v1/ (Slim group /v1). Resources cover full CRUD where applicable:
| Resource | Base path |
|---|---|
| Customers | /customers (incl. address book, basket, wishlist, memos, status history) |
| Categories | /categories |
| Products | /products (incl. images, attributes, tags, xsell, specials, reviews, content) |
| Manufacturers | /manufacturers |
| Attributes | /attributes (options & values) |
| Tags | /tags (options & values) |
| Orders | /orders (incl. products, totals, status history, tracking) |
| Countries | /countries (incl. geo zones, tax classes, tax rates) |
| Shipping | /shipping (carriers & status) |
| Contents | /contents |
| Campaigns | /campaigns |
| Currencies | /currencies |
| Languages | /languages |
| Newsletters | /newsletters |
| Configurations | /configurations |
| Coupons | /coupons |
| DHL | /dhl |
| Schema | /schema/{table} |
| Webhooks | /webhooks (subscriptions, event types, delivery log) |
Instead of polling, external systems (ERPs, apps) can subscribe to shop events and get notified by a signed HTTP POST. Currently available event type: order.created (fired when an order is placed in the checkout).
- Enable webhooks in the module settings (Modules β System β API Access β Webhooks: true). Events are only recorded while this is enabled.
- Set up a cron job - this is required, without it nothing is ever delivered. The dispatcher endpoint is protected by a static secret, stored as
MODULE_API_ACCESS_WEBHOOKS_CRON_SECRETin the shop'sconfigurationtable:The secret may alternatively be passed as*/5 * * * * curl -fsS -H "X-Dispatch-Secret: <cron-secret>" https://your-shop.tld/api/v1/events/dispatch >/dev/null?secret=query parameter for cron panels that cannot set headers - prefer the header, query strings end up in server access logs. Hosts that can run PHP directly can useincludes/external/api/v1/bin/dispatch.phpinstead. Concurrent runs are safe (a database lock makes extra calls no-ops). - Grant the
Webhook*permissions to the API account (Customers β API Access). Subscribing to an event additionally requires the matching read permission (e.g.order.createdrequires OrderGetOrders) -GET /v1/webhooks/event_typesshows all event types and whether the account may subscribe.
curl -X POST https://your-shop.tld/api/v1/webhooks \
-H "Authorization: Bearer <JWT>" -H "Content-Type: application/json" \
-d '{"url": "https://erp.example.com/hooks/shop", "event_types": ["order.created"]}'The URL must be https and publicly reachable. The response contains the signing secret exactly once - store it immediately, it cannot be retrieved again (create a new subscription if it is lost). Manage subscriptions via GET/PUT/DELETE /v1/webhooks/{id}; GET /v1/webhooks/{id}/deliveries shows the recent delivery attempts with status and errors for debugging.
Each delivery is a JSON POST with these headers:
X-Modified-Event: order.created
X-Modified-Delivery: <unique id, stable across retries - use it to deduplicate>
X-Modified-Timestamp: <unix time of this attempt>
X-Modified-Signature: sha256=<hex digest>
{
"id": 123,
"event": "order.created",
"created": 1766999940,
"attempt": 1,
"data": { "orders_id": 4711 },
"links": { "order": "/api/v1/orders/4711" }
}Payloads are deliberately thin (ids only) - fetch the full entity through the regular REST API using the links. Verify every delivery before processing it:
$expected = 'sha256=' . hash_hmac(
'sha256',
$_SERVER['HTTP_X_MODIFIED_TIMESTAMP'] . '.' . file_get_contents('php://input'),
$secret
);
$valid = hash_equals($expected, $_SERVER['HTTP_X_MODIFIED_SIGNATURE'] ?? '')
&& abs(time() - (int)$_SERVER['HTTP_X_MODIFIED_TIMESTAMP']) < 300;Respond with any 2xx status within 10 seconds to acknowledge.
Failed deliveries are retried up to 6 times with increasing backoff (2 min β 10 min β 1 h β 6 h β 24 h). After 10 consecutively failed deliveries a subscription is disabled automatically (disabled_reason says so); fix the receiver and re-enable it via PUT /v1/webhooks/{id} with {"active": true}. Processed events and delivery records are cleaned up automatically after ~30 days.
An interactive Swagger UI is available in the browser:
/api/v1/docs/
It is generated from the codebase and lets you browse every endpoint, inspect the schemas and call the API directly - including the built-in Authorize login (see Authentication). The docs resolve their paths relative to the current install, so they also work when the shop runs in a subdirectory.
The raw OpenAPI 3 specification is served at:
GET /v1/swagger.json
The currently running API version can be checked without authentication:
GET /v1/version
{
"version": "1.0.0",
"requires": "3.2.0"
}requires is the minimum shop version the API needs. The actually running shop version is not exposed.
Contributions are welcome!
If you want to help improve the modified API:
- Fork the repository
- Create a feature branch
- Commit your changes
- Open a pull request
Please keep changes clean, well documented and compatible with existing installations.
Found a bug or have a feature request?
Please use the GitHub Issues section: Open an Issue
- Official Website: https://www.modified-shop.org
- Community Forum: https://www.modified-shop.org/forum
This project is open-source. See the LICENSE file for details.
The modified API is built for real integrations.
The goal is not to chase short-lived API trends, but to provide a stable, well-documented and secure interface that developers and merchants can rely on long-term.