sfcc-scapi-hooks — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited sfcc-scapi-hooks (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
This guide provides essential best practices and code examples for implementing Salesforce Commerce API (SCAPI) hooks. It is designed to be a quick reference for development with AI code assistants.
IMPORTANT: Before implementing SCAPI hooks, consult the Performance and Stability Best Practices guide. Review the index-friendly APIs section and job development standards to ensure your hooks follow SFCC performance requirements and avoid database-intensive operations.
SCAPI hooks are server-side scripts that intercept SCAPI requests to add custom logic. They are used to augment, validate, or modify the behavior of existing API endpoints. For creating entirely new endpoints, use Custom APIs.
For any state-changing request (POST, PATCH, PUT, DELETE), hooks execute in a specific order:
A hook's ability to modify data depends on its transactional context.
| Hook Type | Transactional? | Can Modify Persistent Data? | Primary Purpose |
|---|---|---|---|
before<HTTP_Method> | Yes | Yes | Validation & Preprocessing |
after<HTTP_Method> | Yes | Yes | Business Logic & Side Effects |
modifyResponse | No | No | Formatting the JSON Response |
Note: Attempting to modify persistent data (e.g.,basket.setCustomerEmail()) in amodifyResponsehook will throw an ORM TransactionException.
Hooks must be enabled in Business Manager (Administration > Global Preferences > Feature Switches) and registered in a custom cartridge via two files.
package.json (Cartridge Root)This file points to your hooks configuration.
{
"name": "int_scapi_hooks_extension",
"hooks": "./cartridge/scripts/hooks.json"
}hooks.json (e.g., /cartridge/scripts/hooks.json)This file maps the hook extension point name to your script file.
{
"hooks": [
{
"name": "dw.ocapi.shop.basket.items.beforePOST",
"script": "./hooks/basket/validateItems.js"
},
{
"name": "dw.ocapi.shop.customer.modifyGETResponse",
"script": "./hooks/customer/enrichResponse.js"
},
{
"name": "dw.ocapi.shop.order.afterPOST",
"script": "./hooks/order/notifyOms.js"
}
]
}Organize hook scripts by the resource they modify for better maintainability.
my_cartridge/
├── package.json
└── cartridge/
└── scripts/
├── hooks.json
└── hooks/
├── basket/
│ └── validateItems.js
├── customer/
│ └── enrichResponse.js
└── order/
└── notifyOms.jsHook scripts are CommonJS modules. The exported function name must match the hook's method name (e.g., afterPOST).
'use strict';
var Status = require('dw/system/Status');
/**
* @param {dw.order.Order} order - The newly created order object.
* @returns {dw.system.Status | void}
*/
exports.afterPOST = function (order) {
// Custom logic here
return; // Return void for success to allow hook chain to continue
};dw.system.Status)Use the Status object to control the execution flow.
Controlled Failure: Halts execution and rolls back the transaction. Returns an HTTP 400 error with a fault document.
return new Status(Status.ERROR, 'YOUR_ERROR_CODE', 'A descriptive error message.');Success (Allow Chain to Continue): For Shopper APIs, returning void is the best practice. It allows other hooks in the cartridge path to run.
return;Success (Terminate Chain): Returning Status.OK signals success but stops any subsequent hooks for the same extension point from running.
return new Status(Status.OK);
### Passing Data Between Hooks
If you need to compute something in `after*` and output it in `modify*Response`, use `request.custom` within the same request.
exports.afterPOST = function (basket, doc) { request.custom.myComputedValue = 'abc'; return new Status(Status.OK); };
exports.modifyPOSTResponse = function (basket, responseDoc, doc) { responseDoc.c_myComputedValue = request.custom.myComputedValue; return new Status(Status.OK); };
### Detecting SCAPI vs OCAPI
SCAPI and OCAPI share many hook extension points. When behavior must diverge, branch on `request.isSCAPI()`.Reject adding a restricted product to the cart for non-wholesale customers.
Hook: dw.ocapi.shop.basket.items.beforePOST
'use strict';
var Status = require('dw/system/Status');
var ProductMgr = require('dw/catalog/ProductMgr');
exports.beforePOST = function (basket, items) {
var customer = basket.customer;
var isWholesaleCustomer = customer ? customer.isMemberOfCustomerGroup('Wholesale') : false;
for (var i = 0; i < items.length; i++) {
var product = ProductMgr.getProduct(items[i].product_id);
if (product && product.custom.isRestricted && !isWholesaleCustomer) {
var errorMessage = 'Product ' + product.ID + ' is restricted.';
return new Status(Status.ERROR, 'ITEM_RESTRICTION', errorMessage);
}
}
return; // Success
};Add a calculated c_loyaltyTier attribute to the customer GET response.
Hook: dw.ocapi.shop.customer.modifyGETResponse
'use strict';
var Status = require('dw/system/Status');
exports.modifyGETResponse = function (customer, customerResponse) {
var loyaltyTier = 'Standard';
if (customer.isMemberOfCustomerGroup('GoldMembers')) {
loyaltyTier = 'Gold';
}
// Add a non-persistent attribute to the JSON response
customerResponse.c_loyaltyTier = loyaltyTier;
return new Status(Status.OK);
};Notify an external Order Management System (OMS) after an order is created. The integration is wrapped in a try/catch to prevent an OMS failure from affecting the order creation status.
Hook: dw.ocapi.shop.order.afterPOST
'use strict';
var Status = require('dw/system/Status');
var LocalServiceRegistry = require('dw/svc/LocalServiceRegistry');
var Logger = require('dw/system/Logger').getLogger('OmsIntegration');
exports.afterPOST = function (order) {
try {
var omsService = LocalServiceRegistry.createService('oms.http.service', { /*... service config... */ });
var payload = { orderNo: order.getOrderNo(), total: order.getTotalGrossPrice().getValue() };
var result = omsService.call({ payload: payload });
if (!result.isOk()) {
// Log the error for monitoring, but do NOT return Status.ERROR.
// The order is already created; returning an error here would be misleading.
Logger.error('Failed to notify OMS for order {0}. Error: {1}', order.getOrderNo(), result.getErrorMessage());
}
} catch (e) {
Logger.error('Exception notifying OMS for order {0}. Exception: {1}', order.getOrderNo(), e.toString());
}
// Always return OK because the primary operation (order creation) was successful.
return new Status(Status.OK);
};ProductMgr.getProduct()).basket.customer.ID matches the logged-in shopper's ID.try/catch blocks to prevent unhandled exceptions.dw.system.Logger with custom categories and include the request.requestID for easy tracing in logs.This section provides a reference list of the available hook extension points for the SCAPI Shopper APIs, organized by resource.
| API Endpoint (Method & Path) | Hook Extension Point | Function Signature |
|---|---|---|
POST /baskets | dw.ocapi.shop.basket.beforePOST_v2 | beforePOST_v2(basketRequest : Basket) : dw.system.Status |
POST /baskets | dw.ocapi.shop.basket.afterPOST | afterPOST(basket : dw.order.Basket) : dw.system.Status |
POST /baskets | dw.ocapi.shop.basket.modifyPOSTResponse | modifyPOSTResponse(basket : dw.order.Basket, basketResponse : Basket) : dw.system.Status |
GET /baskets/{basket_id} | dw.ocapi.shop.basket.beforeGET | beforeGET(basketId : String) : dw.system.Status |
GET /baskets/{basket_id} | dw.ocapi.shop.basket.modifyGETResponse | modifyGETResponse(basket : dw.order.Basket, basketResponse : Basket) : dw.system.Status |
PATCH /baskets/{basket_id} | dw.ocapi.shop.basket.beforePATCH | beforePATCH(basket : dw.order.Basket, basketInput : Basket) : dw.system.Status |
PATCH /baskets/{basket_id} | dw.ocapi.shop.basket.afterPATCH | afterPATCH(basket : dw.order.Basket, basketInput : Basket) : dw.system.Status |
PATCH /baskets/{basket_id} | dw.ocapi.shop.basket.modifyPATCHResponse | modifyPATCHResponse(basket : dw.order.Basket, basketResponse : Basket) : dw.system.Status |
DELETE /baskets/{basket_id} | dw.ocapi.shop.basket.beforeDELETE | beforeDELETE(basket : dw.order.Basket) : dw.system.Status |
DELETE /baskets/{basket_id} | dw.ocapi.shop.basket.afterDELETE | afterDELETE(basketId : String) : dw.system.Status |
POST /baskets/{basket_id}/items | dw.ocapi.shop.basket.items.beforePOST | beforePOST(basket : dw.order.Basket, items : ProductItem) : dw.system.Status |
POST /baskets/{basket_id}/items | dw.ocapi.shop.basket.items.afterPOST | afterPOST(basket : dw.order.Basket, items : ProductItem) : dw.system.Status |
POST /baskets/{basket_id}/items | dw.ocapi.shop.basket.items.modifyPOSTResponse | modifyPOSTResponse(basket : dw.order.Basket, basketResponse : Basket, productItems : ProductItem) : dw.system.Status |
POST /baskets/{basket_id}/coupons | dw.ocapi.shop.basket.coupon.beforePOST | beforePOST(basket : dw.order.Basket, couponItem : CouponItem) : dw.system.Status |
POST /baskets/{basket_id}/coupons | dw.ocapi.shop.basket.coupon.afterPOST | afterPOST(basket : dw.order.Basket, couponItem : CouponItem) : dw.system.Status |
POST /baskets/{basket_id}/coupons | dw.ocapi.shop.basket.coupon.modifyPOSTResponse | modifyPOSTResponse(basket : dw.order.Basket, basketResponse : Basket, couponRequest : CouponItem) : dw.system.Status |
POST /baskets/{basket_id}/payment_instruments | dw.ocapi.shop.basket.payment_instrument.beforePOST | beforePOST(basket : dw.order.Basket, paymentInstrument : BasketPaymentInstrumentRequest) : dw.system.Status |
POST /baskets/{basket_id}/payment_instruments | dw.ocapi.shop.basket.payment_instrument.afterPOST | afterPOST(basket : dw.order.Basket, paymentInstrument : BasketPaymentInstrumentRequest) : dw.system.Status |
POST /baskets/{basket_id}/payment_instruments | dw.ocapi.shop.basket.payment_instrument.modifyPOSTResponse | modifyPOSTResponse(basket : dw.order.Basket, basketResponse : Basket, paymentInstrumentRequest : BasketPaymentInstrumentRequest) : dw.system.Status |
| Various | dw.ocapi.shop.basket.validateBasket | validateBasket(basketResponse : Basket, duringSubmit : Boolean) : dw.system.Status |
| API Endpoint (Method & Path) | Hook Extension Point | Function Signature |
|---|---|---|
POST /customers | dw.ocapi.shop.customer.beforePOST | beforePOST(registration : CustomerRegistration) : dw.system.Status |
POST /customers | dw.ocapi.shop.customer.afterPOST | afterPOST(customer : dw.customer.Customer, registration : CustomerRegistration) : dw.system.Status |
POST /customers | dw.ocapi.shop.customer.modifyPOSTResponse | modifyPOSTResponse(customer : dw.customer.Customer, customerResponse : Customer) : dw.system.Status |
GET /customers/{customer_id} | dw.ocapi.shop.customer.beforeGET | beforeGET(customerId : String) : dw.system.Status |
GET /customers/{customer_id} | dw.ocapi.shop.customer.modifyGETResponse | modifyGETResponse(customer : dw.customer.Customer, customerResponse : Customer) : dw.system.Status |
PATCH /customers/{customer_id} | dw.ocapi.shop.customer.beforePATCH | beforePATCH(customer : dw.customer.Customer, customerInput : Customer) : dw.system.Status |
PATCH /customers/{customer_id} | dw.ocapi.shop.customer.afterPATCH | afterPATCH(customer : dw.customer.Customer, customerInput : Customer) : dw.system.Status |
PATCH /customers/{customer_id} | dw.ocapi.shop.customer.modifyPATCHResponse | modifyPATCHResponse(customer : dw.customer.Customer, customerResponse : Customer) : dw.system.Status |
POST /customers/auth | dw.ocapi.shop.auth.beforePOST | beforePOST(authorizationHeader : String, authRequestType : dw.value.EnumValue) : dw.system.Status |
POST /customers/auth | dw.ocapi.shop.auth.afterPOST | afterPOST(customer : dw.customer.Customer, authRequestType : dw.value.EnumValue) : dw.system.Status |
POST /customers/auth | dw.ocapi.shop.auth.modifyPOSTResponse | modifyPOSTResponse(customer : dw.customer.Customer, customerResponse : Customer, authRequestType : dw.value.EnumValue) : dw.system.Status |
PATCH /customers/{customer_id}/addresses/{address_name} | dw.ocapi.shop.customer.address.beforePATCH | beforePATCH(customer : dw.customer.Customer, addressName : String, customerAddress : CustomerAddress) : dw.system.Status |
PATCH /customers/{customer_id}/addresses/{address_name} | dw.ocapi.shop.customer.address.afterPATCH | afterPATCH(customer : dw.customer.Customer, addressName : String, customerAddress : CustomerAddress) : dw.system.Status |
| API Endpoint (Method & Path) | Hook Extension Point | Function Signature |
|---|---|---|
POST /orders | dw.ocapi.shop.order.beforePOST | beforePOST(basket : dw.order.Basket) : dw.system.Status |
POST /orders | dw.ocapi.shop.order.afterPOST | afterPOST(order : dw.order.Order) : dw.system.Status |
POST /orders | dw.ocapi.shop.order.modifyPOSTResponse | modifyPOSTResponse(order : dw.order.Order, orderResponse : Order) : dw.system.Status |
GET /orders/{order_no} | dw.ocapi.shop.order.beforeGET | beforeGET(orderNo : String) : dw.system.Status |
GET /orders/{order_no} | dw.ocapi.shop.order.modifyGETResponse | modifyGETResponse(order : dw.order.Order, orderResponse : Order) : dw.system.Status |
PATCH /orders/{order_no} | dw.ocapi.shop.order.beforePATCH | beforePATCH(order : dw.order.Order, orderInput : Order) : dw.system.Status |
PATCH /orders/{order_no} | dw.ocapi.shop.order.afterPATCH | afterPATCH(order : dw.order.Order, orderInput : Order) : dw.system.Status |
PATCH /orders/{order_no} | dw.ocapi.shop.order.modifyPATCHResponse | modifyPATCHResponse(order : dw.order.Order, orderResponse : Order) : dw.system.Status |
| API Endpoint (Method & Path) | Hook Extension Point | Function Signature |
|---|---|---|
GET /products/{id} | dw.ocapi.shop.product.beforeGET | beforeGET(productId : String) : dw.system.Status |
GET /products/{id} | dw.ocapi.shop.product.modifyGETResponse | modifyGETResponse(scriptProduct : dw.catalog.Product, doc : Product) : dw.system.Status |
GET /product_search | dw.ocapi.shop.product_search.beforeGET | beforeGET() : dw.system.Status |
GET /product_search | dw.ocapi.shop.product_search.modifyGETResponse | modifyGETResponse(doc : ProductSearchResult) : dw.system.Status |
GET /categories/{id} | dw.ocapi.shop.category.beforeGET | beforeGET(categoryId : String) : dw.system.Status |
GET /categories/{id} | dw.ocapi.shop.category.modifyGETResponse | modifyGETResponse(scriptCategory : dw.catalog.Category, doc : Category) : dw.system.Status |
If SCAPI hooks are not executing after deployment:
get_code_versions tool to see all code versions on the instanceactivate_code_version tool to switch code versionsCommon Hook Issues:
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.