← All articles >
magento 2rest apiapi integrationmagento developmentphpe-commercetutorial

Magento 2 REST API Best Practices Guide (2026)

Share: LinkedIn X Facebook

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

  1. REST vs GraphQL: Which One to Use
  2. Authentication Methods
  3. Endpoint Structure and Naming Conventions
  4. Request and Response Formats
  5. Error Handling and Response Codes
  6. Pagination and Filtering
  7. Bulk Operations and Async APIs
  8. Caching and ETag Headers
  9. Creating Custom REST Endpoints
  10. Integration Patterns for ERP/CRM
  11. Testing and Documentation
  12. FAQ

REST vs GraphQL: Which One to Use

Magento 2 supports both REST and GraphQL APIs. Each has different strengths:

ScenarioRecommendation
Admin integrations (ERP, CRM, OMS)REST — wider coverage of admin operations
Storefront/mobile appGraphQL — fewer requests, typed schema
Bulk data syncREST — bulk and async endpoints
Real-time checkoutGraphQL — single mutation for complete checkout
Third-party app accessREST — OAuth 1.0 support

For detailed GraphQL coverage, see the GraphQL API documentation . This guide focuses on REST development and integration.

Authentication Methods

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

EndpointPurpose
GET /rest/V1/products/:skuGet product by SKU
POST /rest/V1/productsCreate product
PUT /rest/V1/products/:skuUpdate product
DELETE /rest/V1/products/:skuDelete product
GET /rest/V1/customers/:idGet customer
GET /rest/V1/orders/:idGet order
POST /rest/V1/cart/mine/orderPlace order (customer cart)
GET /rest/V1/categories/:idGet 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

CodeMeaningWhen to use
200OKSuccessful GET, PUT, DELETE
201CreatedSuccessful POST (resource created)
400Bad RequestMalformed JSON or validation errors
401UnauthorizedMissing or invalid authentication
403ForbiddenAuthenticated but not permitted
404Not FoundResource does not exist
422Unprocessable EntityBusiness logic validation failure
429Too Many RequestsRate limiting
500Internal Server ErrorUnexpected 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.