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 CheckoutExpress Checkout
Initiated fromCheckout page (after address entry)Any merchant page
Shipping/billingCollected by merchant firstCollected by Affirm during flow
Checkout objectIncludes shipping, billing, totalOmits these — only sends items, subtotal, metadata
Server callbackNoneAffirm calls your Shipping & Totals endpoint (server-to-server)
HMAC verificationNot requiredRequired on Shipping & Totals endpoint
Initiated fromCheckout 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_ZONE error code

How To Integrate

Prerequisites

Before enabling Express Checkout, ensure:

  1. Standard Affirm SFRA integration is working — the cartridges int_affirm and int_affirm_sfra are installed, site preferences are configured, and standard Affirm checkout functions correctly.
  2. Cartridge version — you are on the master branch (or a release after 10/02/2026).
  3. Affirm account setup — contact your Affirm Technical Account Manager (TAM) to enable Express Checkout for your merchant account
  4. VCN mode is disabled — Express Checkout is not compatible with Affirm Virtual Card Number (VCN) integration. The AffirmVCNIntegration site preference must be set to off.
  5. Affirm header script is included — Verify that your htmlHead.isml template includes the Affirm SDK script: . This is part of the standard integration and is required for the affirm.checkout() JavaScript API used by Express Checkout.
  6. 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:

MetadataTypePurpose
AffirmExpressCheckoutEnabledSite Preference (boolean)Master toggle for Express Checkout
AffirmSCAPIShortCodeSite Preference (string)SCAPI short code for your instance
AffirmSCAPIOrgIdSite Preference (string)SCAPI organization ID
AffirmSCAPISiteIdSite Preference (string)SFCC site ID for SCAPI calls
AffirmSLASClientIdSite Preference (string)SLAS client ID
AffirmSLASClientSecretSite Preference (password)SLAS client secret
AffirmSCAPIService Profile30s timeout, rate limiting for SCAPI calls
affirm.slas.authServiceSLAS token endpoint
affirm.scapi.basketServiceSCAPI basket operations
affirm.checkout.readServiceReads checkout details from Affirm API v2 after user completes Express Checkout

Verify the Import

After importing, confirm each piece of metadata was created successfully:

  1. Site Preferences — Go to Merchant Tools > Site Preferences > Custom Preferences > AFFIRM_PAYMENT. Scroll to the bottom and verify that the SCAPI/SLAS preferences appear.
  2. Services — Go to Administration > Operations > Services. Verify that affirm.slas.auth and affirm.scapi.basket appear in the service list with the AffirmSCAPI profile assigned.

If any of these are missing, re-run the import and check the import log for errors.

Note: The services.xml schema requires all elements to appear before any elements. If you modify services.xml manually, 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

  1. Ensure your user has the SLAS Organization Administrator role in Account Manager
  2. Go to the SLAS Admin UI at https://{shortCode}.api.commercecloud.salesforce.com/shopper/auth-admin/
  3. Click Clients > Add Client
  4. Select your B2C Commerce instance as the tenant
  5. Choose "BFF or Web App" as the app type (creates a private client with auto-generated secret)
  6. Immediately copy the secret — it is only shown once
  7. Configure:
    1. Channels/Sites: your SFCC site ID (e.g., RefArch, YourSite)
    2. Scopes: sfcc.shopper-baskets-orders.rw, sfcc.shopper-myaccount.baskets.rw, sfcc.shopper-context.rw

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:

  1. AffirmExpressCheckoutEnabled — Set to true
  2. AffirmSCAPIShortCode — Your instance short code (e.g., abcd1234). Found in BM under Administration > Global Preferences > SCAPI Settings.
  3. AffirmSCAPIOrgId — Your SCAPI organization ID (e.g., f_ecom_xxxx_001). Found in the same SCAPI Settings page. Note: use the full format with f_ecom_ prefix.
  4. AffirmSCAPISiteId — Your SFCC site ID (e.g., RefArch). Must match the Channels/Sites value configured on your SLAS client. Leave blank to auto-detect via Site.getCurrent().getID().
  5. AffirmSLASClientId — The client ID from Step 2
  6. AffirmSLASClientSecret — The secret from Step 2
  7. Verify these existing preferences are correctly set:
    1. AffirmOnline — true
    2. AffirmStaticJS — Yes (required for the Affirm SDK to load)
    3. AffirmPublicKey — your Affirm public API key
    4. AffirmPrivateKey — your Affirm private API key (used for HMAC verification)
    5. AffirmVCNIntegration — off (Express Checkout is incompatible with VCN)
    6. AffirmMode — sandbox for testing, production for live

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 js

This 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)

  • 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 checkoutObject for affirm.checkout() call
  • What it does:
    • Validates feature is enabled and VCN is off
    • If pid provided (PDP flow): adds product to storefront basket
    • Gets SLAS token via client_credentials grant
    • 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_url query param
    • Returns the Express Checkout object

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_options array (each with shipping_type, shipping_label, shipping_amount, tax_amount, total)
  • What it does:
    • Verifies HMAC-SHA512 signature from X-Affirm-Signature header
    • Decrypts SCAPI basket ID, shipment ID, and refresh token from the scapi query param
    • Exchanges refresh token for access token (same guest identity)
    • Normalizes country code (USA → US)
    • Validates shipping address (US only)
    • Calls SCAPI setShippingAddress on the SCAPI basket
    • The modifyPUTResponse hook calculates shipping options for all applicable methods
    • Returns HTTP 200 with options, or HTTP 422 with error codes

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)

  • 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-Confirm page

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

  • 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 storefront dw.order.Basket)
  • Returns: the modified body

app.affirm.express.prefilterPossibleShippingMethods

  • Called by: the shippingAddressTotals SCAPI hook's afterPUT, before the per-method calculate loop (Phase 2, Affirm-ShippingTotals)
  • Purpose: narrow which dw.order.ShippingMethod objects 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.Collection of dw.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

  • 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

  • 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

FileCartridgePurpose
scripts/scapi/slasAuth.jsint_affirmSLAS token acquisition (client_credentials + refresh_token grants)
scripts/scapi/scapiBasket.jsint_affirmSCAPI basket CRUD (create, set address, apply coupon, delete)

scripts/basket/affirmBasket.js

(modified)

int_affirmAdds getExpressCheckout() (builds the Express Checkout object) and getSubtotal()

scripts/api/affirmAPI.js

(modified)

int_affirmAdds readCheckout() — fetches checkout details from the Affirm v2 API during ExpressConfirmation
scripts/hooks/scapi/shippingAddressTotals.jsint_affirm_sframodifyPUTResponse hook — calculates all shipping method totals
scripts/hooks.json (modified)int_affirm_sfraRegisters the SCAPI hook
controllers/Affirm.js (modified)int_affirm_sfraAdds the ExpressCheckout, ShippingTotals, and ExpressConfirmation endpoints (see New Controller Endpoints below)
scripts/checkout/affirmOrderFinalize.js (modified)int_affirm_sfraShared 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:

  • AffirmExpressCheckoutEnabled is true
  • AffirmOnline is true
  • AffirmVCNIntegration is not on

Pre-configured placements:

PageTemplate ModifiedButton Location
Product Detail Pagetemplates/default/product/productDetails.ismlAfter "Add to Cart" button
Cart Pagetemplates/default/cart/cart.ismlBefore 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

IssueCauseFix
Express Checkout button not showingAffirmOnline or AffirmExpressCheckoutEnabled is false, or AffirmStaticJS is NoEnable all three in Site Preferences
SLAS auth returns 401Wrong client ID/secret, or client not propagated yetVerify 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 tokenEnsure the encrypted scapi query param is present on the callback URL and decrypts correctly
ShippingTotals returns "Resource Not Found"Wrong SCAPI URL pathVerify shipping-address (hyphen) not shipping_address (underscore)
Affirm returns RPC_ERROR_SHIPPING_OPTIONS_INVALID_MERCHANT_RESPONSECountry code mismatch or response format errorAffirm 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 pathUse ./hooks/scapi/shippingAddressTotals (relative to hooks.json)
services.xml import fails<service-profile> elements after <service> elementsMove all profiles before services in the XML

Log Files

Log FileContains
custom-Affirm-*.logAffirm controller and SCAPI module logs
service-affirm-scapi-*.logSCAPI basket service call details
service-affirm-slas-*.logSLAS authentication call details
customerror-*.logUnhandled errors

Enable Communication Log on the affirm.scapi.basket and affirm.slas.auth services in Administration > Operations > Services for full HTTP request/response logging.



Did this page help you?