Express Checkout on SFCC
This guide provides step-by-step instructions for Salesforce Commerce Cloud (SFCC) merchants to integrate Affirm Express Checkout using the AffirmSFRA cartridge. It assumes you have already completed the standard Affirm SFCC integration. For the platform-agnostic Express Checkout API reference, see the official Affirm Express Checkout documentation.
Overview
Affirm Express Checkout is an Affirm-hosted solution that can be initiated from any merchant hosted page — without requiring the customer's shipping address or final order total upfront. Affirm collects shipping address, shipping method, and financing terms during its hosted checkout flow, then returns the completed details to your storefront for order finalization.
Note: Express Checkout runs alongside the standard Affirm payment method on the checkout page — it does not replace it. Customers can still use the traditional Affirm checkout flow from the payment step. Express Checkout simply provides an additional, faster entry point.
How Express Differs from Standard Affirm Checkout
| Standard Checkout | Express Checkout | |
|---|---|---|
| Initiated from | Checkout page (after address entry) | Any merchant page |
| Shipping/billing | Collected by merchant first | Collected by Affirm during flow |
| Checkout object | Includes shipping, billing, total | Omits these — only sends items, subtotal, metadata |
| Server callback | None | Affirm calls your Shipping & Totals endpoint (server-to-server) |
| HMAC verification | Not required | Required on Shipping & Totals endpoint |
| Initiated from | Checkout page (after address entry) | Any merchant page |
Regional Availability
- United States only — all transactions must be in USD
- Customer shipping address must be a valid US address
- If you don't ship to specific US regions (e.g., Hawaii, Alaska), handle this via the
UNSUPPORTED_SHIPPING_ZONEerror code
How To Integrate
Prerequisites
Before enabling Express Checkout, ensure:
- Standard Affirm SFRA integration is working — the cartridges
int_affirmandint_affirm_sfraare installed, site preferences are configured, and standard Affirm checkout functions correctly. - Cartridge version — you are on the
masterbranch (or a release after 10/02/2026). - Affirm account setup — contact your Affirm Technical Account Manager (TAM) to enable Express Checkout for your merchant account
- VCN mode is disabled — Express Checkout is not compatible with Affirm Virtual Card Number (VCN) integration. The
AffirmVCNIntegrationsite preference must be set tooff. - Affirm header script is included — Verify that your
htmlHead.ismltemplate includes the Affirm SDK script: . This is part of the standard integration and is required for theaffirm.checkout()JavaScript API used by Express Checkout. - SLAS private client configured — Express Checkout uses SCAPI to calculate shipping totals in a sessionless context. This requires a SLAS private client. See Step 2 for setup details.
Step 1: Import Metadata
Import the updated metadata to add the new site preferences and service definitions required by Express Checkout.
Upload metadata/affirm.zip to your instance via Administration > Site Development > Site Import & Export, then select it and run the import.
This single import adds all of the following:
| Metadata | Type | Purpose |
|---|---|---|
AffirmExpressCheckoutEnabled | Site Preference (boolean) | Master toggle for Express Checkout |
AffirmSCAPIShortCode | Site Preference (string) | SCAPI short code for your instance |
AffirmSCAPIOrgId | Site Preference (string) | SCAPI organization ID |
AffirmSCAPISiteId | Site Preference (string) | SFCC site ID for SCAPI calls |
AffirmSLASClientId | Site Preference (string) | SLAS client ID |
AffirmSLASClientSecret | Site Preference (password) | SLAS client secret |
AffirmSCAPI | Service Profile | 30s timeout, rate limiting for SCAPI calls |
affirm.slas.auth | Service | SLAS token endpoint |
affirm.scapi.basket | Service | SCAPI basket operations |
affirm.checkout.read | Service | Reads checkout details from Affirm API v2 after user completes Express Checkout |
Verify the Import
After importing, confirm each piece of metadata was created successfully:
- Site Preferences — Go to Merchant Tools > Site Preferences > Custom Preferences > AFFIRM_PAYMENT. Scroll to the bottom and verify that the SCAPI/SLAS preferences appear.
- Services — Go to Administration > Operations > Services. Verify that
affirm.slas.authandaffirm.scapi.basketappear in the service list with theAffirmSCAPIprofile assigned.
If any of these are missing, re-run the import and check the import log for errors.
Note: The
services.xmlschema requires allelements to appear before any elements. If you modify services.xmlmanually, maintain this ordering.
Step 2: Configure SLAS Private Client
Express Checkout uses SCAPI (Shopper Commerce API) to create a sessionless basket for shipping calculation. This requires a SLAS (Shopper Login and Activity Sequence) private client.
Create the Client
- Ensure your user has the SLAS Organization Administrator role in Account Manager
- Go to the SLAS Admin UI at
https://{shortCode}.api.commercecloud.salesforce.com/shopper/auth-admin/ - Click Clients > Add Client
- Select your B2C Commerce instance as the tenant
- Choose "BFF or Web App" as the app type (creates a private client with auto-generated secret)
- Immediately copy the secret — it is only shown once
- Configure:
- Channels/Sites: your SFCC site ID (e.g.,
RefArch,YourSite) - Scopes:
sfcc.shopper-baskets-orders.rw,sfcc.shopper-myaccount.baskets.rw,sfcc.shopper-context.rw
- Channels/Sites: your SFCC site ID (e.g.,
Important: New SLAS clients can take up to 15 minutes to propagate. Wait before testing.
Test the Client
curl -X POST "https://{shortCode}.api.commercecloud.salesforce.com/shopper/auth/v1/organizations/{orgId}/oauth2/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-u "{clientId}:{clientSecret}" \
-d "grant_type=client_credentials&channel_id={siteId}"
A successful response returns access_token, refresh_token, and expires_in.
Step 3: Configure Site Preference
In Merchant Tools > Site Preferences > Site Custom Preferences > AFFIRM_PAYMENT:
- AffirmExpressCheckoutEnabled — Set to
true - AffirmSCAPIShortCode — Your instance short code (e.g.,
abcd1234). Found in BM under Administration > Global Preferences > SCAPI Settings. - AffirmSCAPIOrgId — Your SCAPI organization ID (e.g.,
f_ecom_xxxx_001). Found in the same SCAPI Settings page. Note: use the full format withf_ecom_prefix. - AffirmSCAPISiteId — Your SFCC site ID (e.g.,
RefArch). Must match the Channels/Sites value configured on your SLAS client. Leave blank to auto-detect viaSite.getCurrent().getID(). - AffirmSLASClientId — The client ID from Step 2
- AffirmSLASClientSecret — The secret from Step 2
- Verify these existing preferences are correctly set:
- AffirmOnline —
true - AffirmStaticJS —
Yes(required for the Affirm SDK to load) - AffirmPublicKey — your Affirm public API key
- AffirmPrivateKey — your Affirm private API key (used for HMAC verification)
- AffirmVCNIntegration —
off(Express Checkout is incompatible with VCN) - AffirmMode —
sandboxfor testing,productionfor live
- AffirmOnline —
Step 4: Enable API Hook Execution
Go to Administration > Global Preferences > Feature Switches and enable API hook execution. This is required for the modifyPUTResponse hook that calculates shipping totals when a shipping address is set on the SCAPI basket.
Step 5: Build Client-Side Assets
The Express Checkout client-side JavaScript needs to be compiled. Ensure your package.json paths reference the base SFRA cartridge correctly, then run:
sgmf-scripts --compile jsThis compiles cartridge/client/default/js/expressCheckout.js into cartridge/static/default/js/expressCheckout.js.
If you are not using a build tool, you can use the pre-built bundle already included at cartridge/static/default/js/expressCheckout.js.
Step 6: Upload Cartridges
Upload the int_affirm and int_affirm_sfra cartridges to your SFCC instance using WebDAV or your preferred IDE (e.g., VS Code with the Prophet extension, Eclipse with UX Studio). Upload them to the active code version directory at:
https://.demandware.net/on/demandware.servlet/webdav/Sites/Cartridges//
Then add them to your site's cartridge path in Administration > Sites > Manage Sites> Select Your Site > Settings. In the Cartridges field, ensure int_affirm_sfra appears before int_affirm:
int_affirm_sfra:int_affirm:...existing_cartridges...
Step 7: Verify Shipping & Totals Endpoint
The Shipping & Totals endpoint URL is automatically passed to Affirm via the merchant_shipping_and_totals_callback_url field in the checkout object. To prevent malicious callback URLs from being injected during checkout creation, Affirm validates the merchant_shipping_and_totals_callback_url hostname against a list of expected hostname patterns registered for your merchant account. Only the hostname is validated; the URL path, path parameters, and query parameters are not checked. Affirm supports and encourages different allowlists per environment e.g., sandbox and production.
To register or update your allowed hostname patterns, contact your Technical Account Manager (TAM) as these allowlists are configured internally by Affirm and are not directly accessible or editable by merchants.
The cartridge generates this URL automatically using URLUtils.https('Affirm-ShippingTotals'), which resolves to the correct hostname for your environment (sandbox or production). You can verify the generated URL by checking the Express Checkout debug logs for the merchant_shipping_and_totals_callback_url value.
Architecture Overview
SCAPI Dual-Basket Approach
The core challenge of Express Checkout is that Affirm's Shipping & Totals callback is a server-to-server request — it has no browser session. SFCC baskets are session-bound (BasketMgr.getCurrentBasket() returns nothing without a session), so we cannot directly calculate shipping on the storefront basket.
The solution: create a second basket via SCAPI (Salesforce Commerce API) during the browser-session phase. SCAPI baskets are accessed via Bearer token, not session, making them accessible in the sessionless S2S callback.
Token Identity & Refresh Tokens
SCAPI baskets are scoped to the guest identity that created them. A different SLAS token (even from the same client) cannot access the basket — it returns "Invalid Customer."
To solve this without server-side storage, we encrypt the SLAS refresh token, SCAPI basket ID, and shipment ID into an opaque query parameter (scapi) on the merchant_shipping_and_totals_callback_url. During the S2S callback, the cartridge decrypts the parameter, exchanges the refresh token for a new access token with the same guest identity, and uses it to access the basket.
The encryption uses AES with the Affirm private key, so the tokens are not exposed in URLs or server logs. The order_id field remains a clean UUID throughout the checkout lifecycle.
End-to-End Flow
Phase 1: ExpressCheckout (Browser Session — GET)
├── Get SLAS token (client_credentials) → access_token + refresh_token
├── Create SCAPI basket with storefront basket items
├── Apply coupons to SCAPI basket
├── Encrypt SCAPI IDs + refresh token → append as ?scapi= query param on callback URL
├── Store SCAPI IDs + token in session (for Cancel/Confirmation cleanup)
└── Return Affirm checkout object with clean order_id (UUID only)
Phase 2: ShippingTotals (S2S, No Session — POST)
├── Decrypt ?scapi= query param → extract basketId, shipmentId, refreshToken
├── Exchange refresh token → new access token (same guest identity)
├── Call SCAPI setShippingAddress on the SCAPI basket
├── SCAPI hooks fire (two-phase design):
│ ├── afterPUT (transactional):
│ │ ├── Iterates all applicable shipping methods
│ │ ├── For each: set method → calculate → capture totals
│ │ └── Stores results on request.custom.affirmShippingTotals
│ └── modifyPUTResponse (read-only):
│ └── Attaches results to response as c_shippingOptions + c_subtotalCents
└── Return shipping_options to Affirm
Phase 3: ExpressConfirmation (Browser Session — POST)
├── Read checkout details from Affirm API (GET /api/v2/checkout/{checkout_id})
├── Apply shipping address, billing address, shipping method to storefront basket
├── Set Affirm payment instrument
├── Recalculate basket
├── Create and place SFCC order
├── Authorize with Affirm, validate totals
├── Send confirmation email
├── Cleanup: delete SCAPI basket (best-effort)
└── Redirect to Order-Confirm page
New Controller Endpoints
The cartridge adds three new endpoints to the Affirm controller:
Affirm-ExpressCheckout (GET)
Affirm-ExpressCheckout (GET)- Called by: Browser (client-side JS)
- Purpose: Create a SCAPI basket and return the Affirm checkout object
- Parameters:
pid(optional, for PDP),quantity(optional, defaults to 1) - Returns: JSON with
checkoutObjectforaffirm.checkout()call - What it does:
- Validates feature is enabled and VCN is off
- If
pidprovided (PDP flow): adds product to storefront basket - Gets SLAS token via
client_credentialsgrant - Creates SCAPI basket mirroring storefront basket items
- Applies coupons from storefront basket to SCAPI basket
- Encrypts SCAPI IDs + refresh token into the
merchant_shipping_and_totals_callback_urlquery param - Returns the Express Checkout object
Affirm-ShippingTotals (POST)
Affirm-ShippingTotals (POST)- Called by: Affirm's backend (server-to-server, sessionless)
- Purpose: Returns available shipping options and totals for a given address
- Headers:
X-Affirm-Signature(HMAC-SHA512),Content-Type: application/json - Body: {order_id, currency, shipping: { line1, city, state, zipcode, country }}
- Returns: JSON with
shipping_optionsarray (each withshipping_type,shipping_label,shipping_amount,tax_amount,total) - What it does:
- Verifies HMAC-SHA512 signature from
X-Affirm-Signatureheader - Decrypts SCAPI basket ID, shipment ID, and refresh token from the
scapiquery param - Exchanges refresh token for access token (same guest identity)
- Normalizes country code (
USA→US) - Validates shipping address (US only)
- Calls SCAPI
setShippingAddresson the SCAPI basket - The
modifyPUTResponsehook calculates shipping options for all applicable methods - Returns HTTP 200 with options, or HTTP 422 with error codes
- Verifies HMAC-SHA512 signature from
Note: Affirm sends the country code in ISO 3166-1 alpha-3 format (e.g.,
"USA"). The cartridge automatically normalizes this to alpha-2 (e.g.,"US") for SCAPI compatibility.
Affirm-ExpressConfirmation (POST)
Affirm-ExpressConfirmation (POST)- Called by: Browser redirect from Affirm after user completes checkout
- Parameter:
checkout_token - What it does:
- Reads checkout details from Affirm API via
GET /api/v2/checkout/{checkout_id} - Applies shipping address, billing address, shipping method, and email to basket
- Recalculates basket totals
- Authorizes the transaction with Affirm
- Creates and places the SFCC order
- Sends confirmation email
- Cleans up SCAPI basket (best-effort)
- Redirects to
Order-Confirmpage
- Reads checkout details from Affirm API via
Extensibility Hooks
The cartridge exposes four optional HookMgr extension points so a merchant cartridge can customize Express Checkout behavior without forking this code. Each is guarded by HookMgr.hasHook — leaving any of them unimplemented falls back to default behavior. Register implementations in your own cartridge's hooks.json.
app.affirm.express.modifyShopperContextBody
app.affirm.express.modifyShopperContextBody- Called by:
scapiBasket.setShopperContext, before the SCAPI shopper-context PUT (Phase 1,Affirm-ExpressCheckout) - Purpose: add or override fields on the shopper-context body sent to SCAPI (e.g.
effectiveDateTime,assignmentQualifiers.store,couponCodes) so storefront promotions and customer-group qualifiers re-fire correctly on the mirrored SCAPI basket - Parameters:
body(the shopper-context request object),basket(the storefrontdw.order.Basket) - Returns: the modified body
app.affirm.express.prefilterPossibleShippingMethods
app.affirm.express.prefilterPossibleShippingMethods- Called by: the
shippingAddressTotalsSCAPI hook'safterPUT, before the per-methodcalculateloop (Phase 2,Affirm-ShippingTotals) - Purpose: narrow which
dw.order.ShippingMethodobjects are considered for a given address before totals are calculated for each one (e.g. exclude store pickup or region-restricted methods). Filtering here as opposed to app.affirm.express.filterShippingMethods has access to less information (amounts) but has the benefit of reducing overall latency by avoiding a SCAPI call. - Parameters:
applicableShippingMethods(dw.util.Collectionofdw.order.ShippingMethod),basket,shipment,shippingAddress(OrderAddressWO) - Returns: the narrowed collection, Note: if you build a new list, return a real dw.util.Collection
app.affirm.express.filterShippingMethods
app.affirm.express.filterShippingMethods- Called by:
Affirm-ShippingTotals, after shipping options are calculated - Purpose: remove, reorder, or relabel already-computed shipping options before they're returned to Affirm
- Parameters:
shippingOptions(array of {shipping_type, shipping_label, shipping_amount, tax_amount, total}),shippingAddress(Object) - Returns: the modified array
app.affirm.express.overrideErrorMessage
app.affirm.express.overrideErrorMessage- Called by:
Affirm-ShippingTotals, for any error response - Purpose: customize the message for a given Affirm error code
- Parameters:
errorCode(string) - Returns: the message to use. This message is not guaranteed to be shown to the customer — Affirm's UI controls final presentation.
New Files
| File | Cartridge | Purpose |
|---|---|---|
scripts/scapi/slasAuth.js | int_affirm | SLAS token acquisition (client_credentials + refresh_token grants) |
scripts/scapi/scapiBasket.js | int_affirm | SCAPI basket CRUD (create, set address, apply coupon, delete) |
(modified) | int_affirm | Adds getExpressCheckout() (builds the Express Checkout object) and getSubtotal() |
(modified) | int_affirm | Adds readCheckout() — fetches checkout details from the Affirm v2 API during ExpressConfirmation |
scripts/hooks/scapi/shippingAddressTotals.js | int_affirm_sfra | modifyPUTResponse hook — calculates all shipping method totals |
scripts/hooks.json (modified) | int_affirm_sfra | Registers the SCAPI hook |
controllers/Affirm.js (modified) | int_affirm_sfra | Adds the ExpressCheckout, ShippingTotals, and ExpressConfirmation endpoints (see New Controller Endpoints below) |
scripts/checkout/affirmOrderFinalize.js (modified) | int_affirm_sfra | Shared order finalization logic (sets payment, authorizes, creates and places the order) used by both standard and Express confirmation |
Template Integration
The cartridge includes out-of-the-box button placement on the PDP and Cart pages. If your storefront uses the standard SFRA templates, the buttons will appear automatically once the feature is enabled.
How It Works
Button template: templates/default/affirm/expressCheckoutButton.isml
This template renders conditionally — it only shows when:
AffirmExpressCheckoutEnabledistrueAffirmOnlineistrueAffirmVCNIntegrationis noton
Pre-configured placements:
| Page | Template Modified | Button Location |
|---|---|---|
| Product Detail Page | templates/default/product/productDetails.isml | After "Add to Cart" button |
| Cart Page | templates/default/cart/cart.isml | Before checkout buttons |
Script loading: templates/default/common/scripts.isml conditionally loads the expressCheckout.js bundle when Express Checkout is enabled.
Customizing Button Placement
If your storefront overrides the default SFRA templates, you need to include the Express Checkout button manually. Add the following ISML include where you want the button to appear:
On PDP:
<isinclude template="affirm/expressCheckoutButton" />With these pdict values set:
<isset name="pageType" value="product" scope="pdict"></isset> <isset name="expressPid" value="${product.id}" scope="pdict"></isset> <isset name="expressQuantity" value="${product.selectedQuantity}" scope="pdict" />On Cart:
<isinclude template="affirm/expressCheckoutButton" />With:
<isset name="pageType" value="cart" scope="pdict" />Customizing Button Appearance
The button supports optional appearance attributes via pdict.expressButtonAttrs:
<isset name="expressButtonAttrs" value="${{
theme: 'dark',
shape: 'pill',
size: 'lg',
buttonText: 'Checkout with Affirm'
}}" scope="pdict" />These map to data-theme, data-shape, data-size, and data-button-text attributes on the button container, which are consumed by the Affirm SDK's affirm.ui.checkoutButton.render() method.
Refreshing Buttons After AJAX Updates
If your storefront dynamically updates page content via AJAX (e.g., cart quantity changes, "Show More" on search results), you must call affirm.ui.refresh() after the DOM update to re-render any Affirm UI elements, including Express Checkout buttons. This is the same requirement as for Affirm promo messaging.
// In your cart.js or wherever you handle AJAX cart updates:
success: function (data) {
// ... update cart HTML ...
if (typeof affirm !== 'undefined') {
affirm.ui.refresh();
}
}Troubleshooting
Common Issues
| Issue | Cause | Fix |
|---|---|---|
| Express Checkout button not showing | AffirmOnline or AffirmExpressCheckoutEnabled is false, or AffirmStaticJS is No | Enable all three in Site Preferences |
| SLAS auth returns 401 | Wrong client ID/secret, or client not propagated yet | Verify credentials match SLAS Admin UI. Wait 15 minutes for new clients. |
| ShippingTotals returns "Invalid Customer" | Fresh SLAS token can't access basket created by different token | Ensure the encrypted scapi query param is present on the callback URL and decrypts correctly |
| ShippingTotals returns "Resource Not Found" | Wrong SCAPI URL path | Verify shipping-address (hyphen) not shipping_address (underscore) |
Affirm returns RPC_ERROR_SHIPPING_OPTIONS_INVALID_MERCHANT_RESPONSE | Country code mismatch or response format error | Affirm sends alpha-3 country codes (e.g., "USA"); cartridge normalizes to alpha-2 for SCAPI. Check response matches Affirm's expected schema. |
hooks.json error: "Path to hooks must be relative" | Hook script path uses */cartridge/... instead of relative path | Use ./hooks/scapi/shippingAddressTotals (relative to hooks.json) |
services.xml import fails | <service-profile> elements after <service> elements | Move all profiles before services in the XML |
Log Files
| Log File | Contains |
|---|---|
custom-Affirm-*.log | Affirm controller and SCAPI module logs |
service-affirm-scapi-*.log | SCAPI basket service call details |
service-affirm-slas-*.log | SLAS authentication call details |
customerror-*.log | Unhandled errors |
Enable Communication Log on the affirm.scapi.basket and affirm.slas.auth services in Administration > Operations > Services for full HTTP request/response logging.
Updated 12 days ago