Magento 2 REST API Best Practices Guide (2026)
Magento 2’s REST API is the backbone of third-party integrations — ERP syncs, CRM connections, mobile apps, and headless storefronts. Adobe continues to invest in REST alongside GraphQL, and it remains the recommended protocol for admin-level integrations and bulk operations. This best practices guide covers endpoint design, authentication, error handling, caching, and production patterns for building reliable Magento 2 REST APIs in 2026.
In short: The REST API exposes resources under
/rest/V1/with standard CRUD verbs. Use token-based authentication for integrations, OAuth 1.0 for third-party apps, and always include proper error responses, pagination, and caching headers. For storefront interactions, see the dedicated GraphQL guide .
Table of Contents
- REST vs GraphQL: Which One to Use
- Authentication Methods
- Endpoint Structure and Naming Conventions
- Request and Response Formats
- Error Handling and Response Codes
- Pagination and Filtering
- Bulk Operations and Async APIs
- Caching and ETag Headers
- Creating Custom REST Endpoints
- Integration Patterns for ERP/CRM
- Testing and Documentation
- FAQ
REST vs GraphQL: Which One to Use
Magento 2 supports both REST and GraphQL APIs. Each has different strengths:
| Scenario | Recommendation |
|---|---|
| Admin integrations (ERP, CRM, OMS) | REST — wider coverage of admin operations |
| Storefront/mobile app | GraphQL — fewer requests, typed schema |
| Bulk data sync | REST — bulk and async endpoints |
| Real-time checkout | GraphQL — single mutation for complete checkout |
| Third-party app access | REST — OAuth 1.0 support |
For detailed GraphQL coverage, see the GraphQL API documentation . This guide focuses on REST development and integration.
Authentication Methods
Integration Tokens (Recommended for Server-to-Server)
Create an integration in Stores → Configuration → Integrations in the admin panel. This generates an access token with specific resource permissions:
# Request token
curl -X POST https://store.com/rest/V1/integration/admin/token \
-H "Content-Type: application/json" \
-d '{"username": "integration_name", "password": "integration_password"}'
# Response: "q7x3p2m9..."
# Use token in subsequent requests
curl https://store.com/rest/V1/products/24-MB01 \
-H "Authorization: Bearer q7x3p2m9..."
Integration tokens are long-lived and scoped to the integration’s API resources. Rotate them periodically via the admin panel.
Admin Token (Use with Caution)
curl -X POST https://store.com/rest/V1/integration/admin/token \
-H "Content-Type: application/json" \
-d '{"username": "admin_user", "password": "admin_password"}'
Admin tokens are tied to an admin user account. When the admin changes their password, all existing tokens are invalidated. Prefer integration tokens for automated integrations.
Customer Tokens
curl -X POST https://store.com/rest/V1/integration/customer/token \
-H "Content-Type: application/json" \
-d '{"username": "[email protected]", "password": "customer_password"}'
Customer tokens are used for storefront-facing integrations (mobile apps, custom frontends). They have customer-level permissions only.
OAuth 1.0
For third-party applications that need delegated access, Magento supports OAuth 1.0 with oauth_token and oauth_token_secret. This is more complex but allows fine-grained permission scoping without sharing admin credentials.
Endpoint Structure and Naming Conventions
Magento 2’s REST API follows a consistent URL pattern:
/rest/V1/{resource}/[id]/[subresource]/[subresource_id]
Built-in Resources
| Endpoint | Purpose |
|---|---|
GET /rest/V1/products/:sku | Get product by SKU |
POST /rest/V1/products | Create product |
PUT /rest/V1/products/:sku | Update product |
DELETE /rest/V1/products/:sku | Delete product |
GET /rest/V1/customers/:id | Get customer |
GET /rest/V1/orders/:id | Get order |
POST /rest/V1/cart/mine/order | Place order (customer cart) |
GET /rest/V1/categories/:id | Get category |
Search Endpoints
Most resources support search via GET /rest/V1/{resource}/search:
GET /rest/V1/products/search?
searchCriteria[filterGroups][0][filters][0][field]=sku&
searchCriteria[filterGroups][0][filters][0][value]=24-MB&
searchCriteria[filterGroups][0][filters][0][conditionType]=like&
searchCriteria[pageSize]=20&
searchCriteria[currentPage]=1
Response includes total count and items. Always implement pagination — never request all records at once.
Request and Response Formats
All requests and responses use application/json. Follow Magento’s standard structures:
Request:
{
"product": {
"sku": "CUSTOM-SKU-001",
"name": "Custom Product",
"price": 29.99,
"status": 1,
"visibility": 4,
"type_id": "simple",
"attribute_set_id": 4,
"extension_attributes": {
"stock_item": {
"qty": 100,
"is_in_stock": true
}
},
"custom_attributes": [
{
"attribute_code": "description",
"value": "Product description here"
}
]
}
}
Successful response (200/201):
{
"id": 42,
"sku": "CUSTOM-SKU-001",
"name": "Custom Product",
...
}
Error Handling and Response Codes
A well-designed API returns appropriate HTTP status codes and structured error bodies.
Standard HTTP Status Codes
| Code | Meaning | When to use |
|---|---|---|
200 | OK | Successful GET, PUT, DELETE |
201 | Created | Successful POST (resource created) |
400 | Bad Request | Malformed JSON or validation errors |
401 | Unauthorized | Missing or invalid authentication |
403 | Forbidden | Authenticated but not permitted |
404 | Not Found | Resource does not exist |
422 | Unprocessable Entity | Business logic validation failure |
429 | Too Many Requests | Rate limiting |
500 | Internal Server Error | Unexpected server error |
Error Response Structure
Magento’s standard error format:
{
"message": "Product with SKU \"CUSTOM-SKU-001\" already exists.",
"trace": "...",
"parameters": {
"sku": "CUSTOM-SKU-001"
}
}
For custom endpoints, return structured errors consistently:
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Webapi\Exception as WebapiException;
// Validation error
throw new WebapiException(
__('Product SKU is required'),
0,
WebapiException::HTTP_BAD_REQUEST
);
// Resource not found
throw new LocalizedException(
__('Product with SKU "%1" not found.', $sku)
);
Pagination and Filtering
Always paginate collection endpoints. Use Magento’s searchCriteria patterns consistently:
GET /rest/V1/products/search?
searchCriteria[filterGroups][0][filters][0][field]=price&
searchCriteria[filterGroups][0][filters][0][value]=50&
searchCriteria[filterGroups][0][filters][0][conditionType]=gteq&
searchCriteria[sortOrders][0][field]=created_at&
searchCriteria[sortOrders][0][direction]=DESC&
searchCriteria[pageSize]=50&
searchCriteria[currentPage]=2
Response includes total count for client-side pagination UI:
{
"total_count": 342,
"items": [ ... ]
}
Bulk Operations and Async APIs
For large data syncs, use Magento’s bulk API endpoints:
# Async bulk product creation
POST /rest/V1/async/bulk/V1/products
Content-Type: application/json
Authorization: Bearer {token}
[
{ "product": { "sku": "BULK-001", "name": "Bulk 1", "price": 10 } },
{ "product": { "sku": "BULK-002", "name": "Bulk 2", "price": 20 } },
...
]
The async endpoint returns a bulk UUID immediately and processes the operations in the background via message queues. Poll the status via:
GET /rest/V1/bulk/{bulkUuid}/status
Bulk operations are essential for ERP/CRM integrations that sync thousands of products, customers, or orders. They prevent API timeouts and reduce server load.
Caching and ETag Headers
The REST API respects standard HTTP caching. For read endpoints that return data which changes infrequently (categories, CMS blocks, store config), include caching headers:
// In your custom endpoint
$this->resultFactory->create(ResultFactory::TYPE_JSON)
->setData($data)
->setHeader('Cache-Control', 'public, max-age=3600')
->setHeader('ETag', md5(serialize($data)));
Clients can then send If-None-Match headers to receive 304 Not Modified responses when data hasn’t changed — saving bandwidth and processing time.
Creating Custom REST Endpoints
1. Define routes in webapi.xml
<!-- app/code/Vendor/Module/etc/webapi.xml -->
<routes xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Webapi:etc/webapi.xsd">
<route url="/V1/vendor/price-check/:sku" method="GET">
<service class="Vendor\Module\Api\PriceManagementInterface" method="getPrice"/>
<resources>
<resource ref="anonymous"/>
</resources>
</route>
</routes>
The resources section defines permissions. Use anonymous for public endpoints, or reference specific ACL resources for admin-only endpoints.
2. Create the service interface
<?php
declare(strict_types=1);
namespace Vendor\Module\Api;
interface PriceManagementInterface
{
/**
* Get current price and special price for a product
*
* @param string $sku
* @return \Vendor\Module\Api\Data\PriceDataInterface
* @throws \Magento\Framework\Exception\NoSuchEntityException
*/
public function getPrice(string $sku): \Vendor\Module\Api\Data\PriceDataInterface;
}
3. Create the data interface
<?php
declare(strict_types=1);
namespace Vendor\Module\Api\Data;
interface PriceDataInterface
{
/**
* @return float
*/
public function getPrice(): float;
/**
* @param float $price
* @return $this
*/
public function setPrice(float $price): self;
/**
* @return float|null
*/
public function getSpecialPrice(): ?float;
/**
* @param float|null $specialPrice
* @return $this
*/
public function setSpecialPrice(?float $specialPrice): self;
}
4. Implement the service
<?php
declare(strict_types=1);
namespace Vendor\Module\Model;
use Vendor\Module\Api\PriceManagementInterface;
use Vendor\Module\Api\Data\PriceDataInterfaceFactory;
use Magento\Catalog\Api\ProductRepositoryInterface;
class PriceManagement implements PriceManagementInterface
{
public function __construct(
private readonly ProductRepositoryInterface $productRepository,
private readonly PriceDataInterfaceFactory $priceDataFactory
) {}
public function getPrice(string $sku): PriceDataInterface
{
$product = $this->productRepository->get($sku);
$priceData = $this->priceDataFactory->create();
$priceData->setPrice((float)$product->getPrice());
$priceData->setSpecialPrice($product->getSpecialPrice() !== null
? (float)$product->getSpecialPrice()
: null);
return $priceData;
}
}
5. Add extension_attributes for extensibility
<!-- app/code/Vendor/Module/etc/extension_attributes.xml -->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Api/etc/extension_attributes.xsd">
<extension_attributes for="Magento\Catalog\Api\Data\ProductInterface">
<attribute code="custom_badge" type="string"/>
</extension_attributes>
</config>
Integration Patterns for ERP/CRM
Order Sync (Magento → ERP)
Use Magento’s sales_order_save_after observer
to queue order data for export. Process via cron job or message queue:
Order placed → Observer fires → Order data queued in database
↓
Cron job picks up queue
↓
POST to ERP endpoint
↓
Mark order as exported
Inventory Sync (ERP → Magento)
Use the REST bulk API for inventory updates:
POST /rest/V1/async/bulk/V1/products/bySkus/stockItems
[
{ "sku": "PROD-001", "qty": 50, "is_in_stock": true },
{ "sku": "PROD-002", "qty": 0, "is_in_stock": false }
]
Run this as a scheduled job from the ERP side every 5–15 minutes depending on inventory volatility.
Customer Push (CRM → Magento)
POST /rest/V1/async/bulk/V1/customers
[
{
"customer": {
"email": "[email protected]",
"firstname": "John",
"lastname": "Doe",
"website_id": 1,
"store_id": 1,
"group_id": 1
},
"password": "temporary_password"
}
]
Send welcome emails from Magento (never from the external system) to maintain consistent branding and deliverability.
Testing and Documentation
Testing with Postman Collection
Export your Magento 2 API endpoints as a Postman collection. Include:
- Environment variables for base URL, token
- Pre-request scripts for token generation
- Example bodies for each endpoint
- Test scripts validating response structure
Automated API Tests
# Use Magento's integration test framework
vendor/bin/phpunit -c dev/tests/integration/phpunit.xml \
--filter testPriceEndpoint
OpenAPI/Swagger Documentation
Magento 2 generates Swagger documentation at /rest/V1/swagger in developer mode. For custom endpoints, annotate interfaces with OpenAPI tags:
/**
* Get product price
*
* @api
* @param string $sku
* @return PriceDataInterface
* @throws NoSuchEntityException
*/
public function getPrice(string $sku): PriceDataInterface;
FAQ
Q: What is the difference between REST and GraphQL in Magento 2?
REST follows resource-based URL patterns and is better for admin integrations. GraphQL is query-based and better for storefront/mobile apps. See the GraphQL comparison guide
for details.
Q: How do I handle rate limiting for the REST API?
Magento 2 does not have built-in rate limiting. Implement it at the web server level (Nginx limit_req_zone) or use a CDN that supports rate limiting.
Q: Can I use the REST API for checkout?
Yes, but GraphQL is preferred for storefront checkout because it reduces the number of requests. REST checkout requires multiple calls (add to cart, set shipping, set payment, place order).
Q: How do I secure API endpoints for public access?
Use resource ref="anonymous" in webapi.xml for public endpoints. For endpoints that need some validation without full authentication, implement a custom HMAC signature check or API key header validation in a plugin.
Q: What is the maximum payload size for bulk API requests?
This depends on your PHP configuration (upload_max_filesize, post_max_size) and web server limits. For very large payloads (10,000+ items), split into batches of 100–500 and use the async bulk API.
Q: How do I test custom endpoints locally?
Use CURL, Postman, or Insomnia. Set app/etc/env.php MAGE_MODE to developer to see detailed error messages. Magento also provides integration test suites for API testing.
Need a custom API integration for your Magento 2 store? Check my API & integrations service and custom modules service . Also see the GraphQL storefront guide for storefront API patterns.