home / skills / salesforcecommercecloud / b2c-developer-tooling / b2c-hooks
This skill helps you implement and manage B2C hooks across OCAPI, SCAPI, and system extensions to customize commerce workflows.
npx playbooks add skill salesforcecommercecloud/b2c-developer-tooling --skill b2c-hooksReview the files below or copy the command above to add this skill to your agents.
---
name: b2c-hooks
description: Implement hooks using HookMgr and extension points in B2C Commerce. Use when extending OCAPI/SCAPI behavior, handling system events like order calculation, or registering custom hook implementations. Covers hooks.json, dw.ocapi hooks, and custom extension points.
---
# B2C Commerce Hooks
Hooks are extension points that allow you to customize business logic by registering scripts. B2C Commerce supports two types of hooks:
1. **OCAPI/SCAPI Hooks** - Extend API resources with before, after, and modifyResponse hooks
2. **System Hooks** - Custom extension points for order calculation, payment, and other core functionality
## Hook Types Overview
| Type | Purpose | Examples |
|------|---------|----------|
| OCAPI/SCAPI | Extend API behavior | `dw.ocapi.shop.basket.afterPOST` |
| System | Core business logic | `dw.order.calculate` |
| Custom | Your own extension points | `app.checkout.validate` |
## Hook Registration
### File Structure
```
my_cartridge/
├── package.json # References hooks.json
└── cartridge/
└── scripts/
├── hooks.json # Hook registrations
└── hooks/ # Hook implementations
├── basket.js
└── order.js
```
### package.json
Reference the hooks configuration file:
```json
{
"name": "my_cartridge",
"hooks": "./cartridge/scripts/hooks.json"
}
```
### hooks.json
Register hooks with their implementing scripts:
```json
{
"hooks": [
{
"name": "dw.ocapi.shop.basket.afterPOST",
"script": "./hooks/basket.js"
},
{
"name": "dw.ocapi.shop.basket.modifyPOSTResponse",
"script": "./hooks/basket.js"
},
{
"name": "dw.order.calculate",
"script": "./hooks/order.js"
}
]
}
```
### Hook Script
Export functions matching the hook method name (without package prefix):
```javascript
// hooks/basket.js
var Status = require('dw/system/Status');
exports.afterPOST = function(basket) {
// Called after basket creation
// Returning a value would skip system implementation
};
exports.modifyPOSTResponse = function(basket, basketResponse) {
// Modify the API response
basketResponse.c_customField = 'value';
};
```
## HookMgr API
Use `dw.system.HookMgr` to call hooks programmatically:
```javascript
var HookMgr = require('dw/system/HookMgr');
// Check if hook exists
if (HookMgr.hasHook('dw.order.calculate')) {
// Call the hook
var result = HookMgr.callHook('dw.order.calculate', 'calculate', basket);
}
```
| Method | Description |
|--------|-------------|
| `hasHook(extensionPoint)` | Returns true if hook is registered or has default implementation |
| `callHook(extensionPoint, functionName, args...)` | Calls the hook, returns result or undefined |
## Status Object
Hooks return `dw.system.Status` to indicate success or failure:
```javascript
var Status = require('dw/system/Status');
// Success - continue processing
return new Status(Status.OK);
// Error - stop processing, rollback transaction
var status = new Status(Status.ERROR);
status.addDetail('error_code', 'INVALID_ADDRESS');
status.addDetail('message', 'Address validation failed');
return status;
```
| Status | HTTP Response | Behavior |
|--------|---------------|----------|
| `Status.OK` | Continues | Hook execution continues |
| `Status.ERROR` | 400 Bad Request | Transaction rolled back, processing stops |
| Uncaught exception | 500 Internal Error | Transaction rolled back |
## Return Value Behavior (Important)
**OCAPI/SCAPI hooks that return ANY value will SKIP the system implementation and all subsequent registered hooks for that extension point.**
This is a common source of bugs. For example, if a hook returns `Status.OK`, the system's `dw.order.calculate` implementation won't run, causing cart totals to be incorrect.
### When to Return a Value
Return a `Status` object **only** when you want to:
- **Stop processing** with an error (`Status.ERROR`)
- **Skip the system implementation** intentionally
### When NOT to Return a Value
To ensure system implementations run (like cart calculation), **return nothing**:
```javascript
// Returning Status.OK skips system implementation
exports.afterPOST = function(basket) {
doSomething(basket);
return new Status(Status.OK); // Skips dw.order.calculate
};
// No return value - system implementation runs
exports.afterPOST = function(basket) {
doSomething(basket);
// No return, or explicit: return;
};
```
### Summary
| Return Value | OCAPI/SCAPI Behavior | Custom Hook Behavior |
|-------------|---------------------|---------------------|
| `undefined` (no return) | System implementation runs, subsequent hooks run | All hooks run |
| `Status.OK` | **Skips** system implementation and subsequent hooks | All hooks run |
| `Status.ERROR` | Stops processing, returns error | All hooks run |
**Debugging tip**: If cart totals are wrong or hooks aren't firing, check if an earlier hook is returning a value.
## OCAPI/SCAPI Hooks
OCAPI and SCAPI share the same hooks. Enable in Business Manager:
**Administration > Global Preferences > Feature Switches > Enable Salesforce Commerce Cloud API hook execution**
### Hook Types
| Hook | When Called | Use Case |
|------|-------------|----------|
| `before<METHOD>` | Before processing | Validation, access control |
| `after<METHOD>` | After processing (in transaction) | Data modification, external calls |
| `modify<METHOD>Response` | Before response sent | Add/modify response properties |
### Common Hook Patterns
```javascript
// Validation in beforePUT
exports.beforePUT = function(basket, addressDoc) {
if (!isValidAddress(addressDoc)) {
var status = new Status(Status.ERROR);
status.addDetail('validation_error', 'Invalid address');
return status;
}
};
// External call in afterPOST (within transaction)
exports.afterPOST = function(basket, paymentDoc) {
var result = callPaymentService(paymentDoc);
request.custom.paymentResult = result; // Pass to modifyResponse
// Returning a Status would skip system implementation
};
// Modify response
exports.modifyPOSTResponse = function(basket, basketResponse, paymentDoc) {
basketResponse.c_paymentStatus = request.custom.paymentResult.status;
};
```
### Passing Data Between Hooks
Use `request.custom` to pass data between hooks in the same request:
```javascript
// In afterPOST
exports.afterPOST = function(basket, doc) {
request.custom.externalId = callExternalService();
};
// In modifyPOSTResponse
exports.modifyPOSTResponse = function(basket, response, doc) {
response.c_externalId = request.custom.externalId;
};
```
### Detect SCAPI vs OCAPI
```javascript
exports.afterPOST = function(basket) {
if (request.isSCAPI()) {
// SCAPI-specific logic
} else {
// OCAPI-specific logic
}
};
```
## System Hooks
### Calculate Hooks
| Extension Point | Function | Purpose |
|-----------------|----------|---------|
| `dw.order.calculate` | `calculate` | Full basket/order calculation |
| `dw.order.calculateShipping` | `calculateShipping` | Shipping calculation |
| `dw.order.calculateTax` | `calculateTax` | Tax calculation |
```javascript
// hooks/calculate.js
var Status = require('dw/system/Status');
var HookMgr = require('dw/system/HookMgr');
exports.calculate = function(lineItemCtnr) {
// Calculate shipping
HookMgr.callHook('dw.order.calculateShipping', 'calculateShipping', lineItemCtnr);
// Calculate promotions, totals...
// Calculate tax
HookMgr.callHook('dw.order.calculateTax', 'calculateTax', lineItemCtnr);
return new Status(Status.OK);
};
```
### Payment Hooks
| Extension Point | Function | Purpose |
|-----------------|----------|---------|
| `dw.order.payment.authorize` | `authorize` | Payment authorization |
| `dw.order.payment.capture` | `capture` | Capture authorized payment |
| `dw.order.payment.refund` | `refund` | Refund payment |
| `dw.order.payment.validateAuthorization` | `validateAuthorization` | Check authorization validity |
| `dw.order.payment.reauthorize` | `reauthorize` | Re-authorize expired auth |
### Order Hooks
| Extension Point | Function | Purpose |
|-----------------|----------|---------|
| `dw.order.createOrderNo` | `createOrderNo` | Custom order number generation |
```javascript
var OrderMgr = require('dw/order/OrderMgr');
var Site = require('dw/system/Site');
exports.createOrderNo = function() {
var seqNo = OrderMgr.createOrderSequenceNo();
var prefix = Site.current.ID;
return prefix + '-' + seqNo;
};
```
## Custom Hooks
Create your own extension points:
```javascript
// Define custom hook
var HookMgr = require('dw/system/HookMgr');
function processCheckout(basket) {
// Call custom hook if registered
if (HookMgr.hasHook('app.checkout.validate')) {
var status = HookMgr.callHook('app.checkout.validate', 'validate', basket);
if (status && status.error) {
return status;
}
}
// Continue processing...
}
```
Register in hooks.json:
```json
{
"hooks": [
{
"name": "app.checkout.validate",
"script": "./hooks/checkout.js"
}
]
}
```
Custom hooks always execute all registered implementations regardless of return value.
## Remote Includes in Hooks
Enhance API responses with data from other SCAPI endpoints:
```javascript
var RESTResponseMgr = require('dw/system/RESTResponseMgr');
exports.modifyGETResponse = function(product, doc) {
// Include Custom API response
var include = RESTResponseMgr.createScapiRemoteInclude(
'custom', // API family
'my-api', // API name
'v1', // Version
'endpoint' // Endpoint
);
doc.c_additionalData = { value: [include] };
};
```
## Best Practices
- Return `undefined` (no return) from OCAPI/SCAPI hooks to ensure system implementations run
- Only return `Status.ERROR` when you need to stop processing
- Returning `Status.OK` skips system implementation and subsequent hooks
- Use `request.custom` to pass data between hooks
- Check `request.isSCAPI()` when supporting both APIs
- Keep hooks focused and performant
- Use custom properties (`c_` prefix) in modifyResponse
- Avoid transactions in calculate hooks (breaks SCAPI)
- Avoid slow external calls in beforeGET (affects caching)
## Error Handling
### Circuit Breaker
Too many hook errors triggers circuit breaker (HTTP 503):
```json
{
"title": "Hook Circuit Breaker",
"type": "https://api.commercecloud.salesforce.com/.../hook-circuit-breaker",
"detail": "Failure rate above threshold of '50%'",
"extensionPointName": "dw.ocapi.shop.basket.afterPOST"
}
```
### Timeout
Hooks must complete within the SCAPI timeout (HTTP 504 on timeout).
## Detailed References
- [OCAPI/SCAPI Hooks](references/OCAPI-SCAPI-HOOKS.md) - API hook patterns and available hooks
- [System Hooks](references/SYSTEM-HOOKS.md) - Calculate, payment, and order hooks
This skill documents how to implement and register hooks in B2C Commerce using HookMgr and hooks.json. It covers OCAPI/SCAPI hooks, system hooks (calculate, payment, order), and creating custom extension points. The goal is to help developers extend API behavior and core processing without breaking system implementations.
Hooks are registered in a cartridge hooks.json and implemented as scripts that export functions matching hook method names (e.g., afterPOST, calculate). Use dw.system.HookMgr.hasHook and .callHook to detect and invoke hooks programmatically. OCAPI/SCAPI hooks run around API methods (before/after/modifyResponse), while system hooks handle core logic like order calculation or payment flows. Custom hooks always execute all implementations regardless of return values.
What happens if a hook returns a value?
For OCAPI/SCAPI hooks any non-undefined return (including Status.OK) stops subsequent hooks and skips the system implementation; only return a Status when you want to stop or intentionally replace system behavior.
How do I call a hook from code?
Use dw.system.HookMgr.hasHook('extensionPoint') to check and HookMgr.callHook('extensionPoint','functionName', args...) to invoke it and get its result or Status.