Back to Website
Product Documentation Operations Portal Punchout Punchout Customization Guide

Punchout Customization Guide


1. Interceptors Overview


Punchout integration often requires customization to accommodate different procurement systems and business requirements. Kodaris Commerce provides a powerful interceptor framework that allows you to customize various aspects of the punchout flow without modifying core code.

Interceptors are JavaScript functions that are executed at specific points in the punchout process. They allow you to:

  • Modify XML requests and responses
  • Customize authentication logic
  • Transform cart data before transmission
  • Map addresses between systems
  • Add provider-specific adaptations

2. Available Interceptor Points


Kodaris Commerce provides the following interceptor points for customizing the punchout process:

Interceptor NameExecution PointAvailable VariablesPurpose
punchOutAuthentication.jsDuring authentication of PunchOutSetupRequestintegrationCustomer, xmlRequest, header, contact, newIntegrationUserNameCustomize user authentication and selection
punchOutSetupRequestSuccessResponse.jsBefore sending PunchOutSetupResponsexmlRequest, xmlResponseModify PunchOutSetupResponse XML
cartPunchOutOrderMessage.jsDuring generation of PunchOutOrderMessagepunchOutOrderMessage, punchOutOrderMessageCXML, punchOutSetupRequest, punchOutSetupRequestCXML, orderCustomize cart data before transmission
punchOutOrderMessageSuccessResponse.jsAfter generating PunchOutOrderMessagexmlRequest, xmlResponseModify final PunchOutOrderMessage XML
punchOutOrderRequestSuccessResponse.jsBefore sending response to PunchOutOrderRequestxmlRequest, xmlResponseModify PunchOutOrderRequest response XML
punchoutMapAddressIdToERP.jsDuring order processingerpAddressCode, punchoutAddressCode, isShipping, isBilling, customer, companyCode, punchoutSystemMap punchout address IDs to ERP address codes

3. Authentication Customization


3.1 Dynamic User Authentication

One common customization is to dynamically authenticate users based on information in the PunchOutSetupRequest, such as a ShipTo address ID or other attributes.

// Script designed to run on PunchOutSetupRequest right before sending back PunchOutSetupResponse
// Vars in scope:
// - integrationCustomer    - Object - Authenticated Customer
// - xmlRequest             - String - Original incoming SetupRequest XML
// - header                 - Object - Header element from PunchOut request
// - contact                - Object - Contact element from Request > PunchOutSetupRequest
// - newIntegrationUserName - String - Can be set to change the authenticated customer

// Check the parent company authentication to know when to switch accounts
if (integrationCustomer && integrationCustomer.userName === 'ParentCompany@example.com') {
    
    // Extract ShipTo addressID from XML request
    var shiptoAddressID;
    var regex = /ShipTo>\s*/g;
    var match;
    
    while ((match = regex.exec(xmlRequest)) !== null) {
        shiptoAddressID = match[1];
        console.log('Found ShipTo > Address.addressID = ' + shiptoAddressID);
    }
    
    // Determine if we're handling a setup request or an order
    var isPunchOutSetupRequest = xmlRequest.indexOf('
    

This example shows how to authenticate different users based on the ShipTo address ID in the PunchOutSetupRequest. This is particularly useful for organizations with multiple locations or departments that need to be treated as separate accounts.

3.2 Customizing Authentication Response

Some procurement systems require specific formats or values in the PunchOutSetupResponse. You can customize the response XML to meet these requirements.

// Script designed to run on PunchOutSetupRequest right before sending back PunchOutSetupResponse
// Vars in scope:
// - xmlRequest  - PunchOutSetupRequest XML string
// - xmlResponse - PunchOutSetupResponse XML string

// For Ariba PunchOut provider requests
if (xmlRequest.match("ariba\\.com")) {

    // Remove standalone attribute from xml header
    xmlResponse = xmlResponse.replace(" standalone=\"yes\"", "");

    // Change success text status code from OK to Accepted (Ariba preference)
    xmlResponse = xmlResponse.replace("text=\"OK\"", "text=\"Accepted\"");
    
    // Get custom redirect URL from company settings
    var redirectUrl = "";
    try {
        if (integrationCustomer.companyID) {
            var result = scriptServiceUtils.runAPIMethod(
                'GET', 
                '/api/system/company/{companyID}/setting/{code}', 
                {"companyID": integrationCustomer.companyID, "code": "punchoutRedirectUrl"}, 
                {}, 
                {});
            
            if (result && result.value) {
                redirectUrl = result.value;
            }
        }
    } catch(ex) {
        console.log("Exception finding redirectUrl: " + ex);
    }

    // Modify the URL in the response to include custom redirect
    var newUrl = "example.com/api/user/customer/access/login/PUNCHOUT?";
    if (redirectUrl) {
        newUrl = newUrl + "redirectUri=" + redirectUrl + "&";
    }
    
    xmlResponse = xmlResponse.replace("shop.example.com/api/user/customer/access/login/PUNCHOUT?", newUrl);
}

Ariba Specific Requirements: Ariba often has specific requirements for XML responses, such as:

  • Using "Accepted" instead of "OK" in status responses
  • Removing the standalone attribute from XML declarations
  • Custom formatting of URLs

4. Cart Customization


4.1 PunchOutOrderMessage Customization

When users check out and return to the procurement system, you can customize the PunchOutOrderMessage XML that contains the cart data.

// Script designed to run at the end of OrderMessage (Cart) cXML generation
// Vars in scope:
// - punchOutOrderMessage - PunchOutOrderMessage object
// - punchOutOrderMessageCXML - Full CXML for PunchOutOrderMessage
// - punchOutSetupRequest - PunchOutSetupRequest object that originated the session
// - punchOutSetupRequestCXML - Full CXML for PunchOutSetupRequest
// - order - Order object (Cart)

// Example: Map handling fee to shipping cost for a specific provider
if (punchOutSetupRequestCXML.payloadID.indexOf("specificProvider") !== -1) {
    for (var i = 0; i < order.orderDiscounts.length; i++) {
        if ("handling fee" === order.orderDiscounts[i].name.toLowerCase()) {
            var handlingFeeAmount = order.orderDiscounts[i].reward;
            console.log("Handling Fee found, applying as Shipping, amount = " + handlingFeeAmount);
            punchOutOrderMessage.punchOutOrderMessageHeader.shipping.money.value = handlingFeeAmount;
            break;
        }
    }
}

// Example: Customize classification for products
if (punchOutOrderMessage && punchOutOrderMessage.itemIn && punchOutOrderMessage.itemIn.length > 0) {
    for (var i = 0; i < punchOutOrderMessage.itemIn.length; i++) {
        // Get the product code
        var productCode = punchOutOrderMessage.itemIn[i].itemID.supplierPartID.value;
        
        // Find additional product information
        var pimProduct = findProductByCode(productCode);
        
        if (pimProduct) {
            // Set UNSPSC classification if available
            if (punchOutOrderMessage.itemIn[i].itemDetail.classification && 
                punchOutOrderMessage.itemIn[i].itemDetail.classification.length > 0) {
                
                punchOutOrderMessage.itemIn[i].itemDetail.classification[0].domain = 'UNSPSC';
                
                if (pimProduct.unspscCode) {
                    punchOutOrderMessage.itemIn[i].itemDetail.classification[0].value = pimProduct.unspscCode;
                }
            }
            
            // Add additional product attributes as Extrinsic elements
            if (pimProduct.attributes) {
                for (var attr in pimProduct.attributes) {
                    var extrinsic = new Extrinsic();
                    extrinsic.name = attr;
                    extrinsic.value = pimProduct.attributes[attr];
                    punchOutOrderMessage.itemIn[i].itemDetail.extrinsic.add(extrinsic);
                }
            }
        }
    }
}

// Helper function to find product information
function findProductByCode(code) {
    var params = {
        "filterFields": [
            {
                "name": "code",
                "operation": "IS",
                "value": code
            }
        ],
        page: 0,
        size: 1
    };
    
    var result = scriptServiceUtils.runAPIMethod('POST', '/api/system/product/search', null, null, params);
    if (result && result.content && result.content.length > 0) {
        return result.content[0];
    }
    
    return null;
}

4.2 Provider-Specific Cart Adaptations

Different procurement systems often have specific requirements for cart data. Here are examples of customizations for common providers:

Coupa Customizations

// Coupa-specific customizations
if (punchOutSetupRequestCXML.payloadID.indexOf("coupa") !== -1) {
    for (var i=0; i < punchOutOrderMessage.itemIn.length; i++) {
        // Normalize unit of measure to EA (Each) if it's "each"
        if ("each" === punchOutOrderMessage.itemIn[i].itemDetail.unitOfMeasure.toLowerCase()) {
            punchOutOrderMessage.itemIn[i].itemDetail.unitOfMeasure = "EA";
        }
        
        // Add UNSPSC classification
        var productCode = punchOutOrderMessage.itemIn[i].itemID.supplierPartID.value;
        var searchParams = {
            "filterFields": [
                {
                    "name": "code",
                    "operation": "IS",
                    "value": productCode
                }
            ]
        };
        
        try {
            var result = scriptServiceUtils.runAPIMethod('POST', '/api/system/product/search', null, null, searchParams);
            
            if(result && result.totalElements >= 1 && result.content[0].unspscCode) {
                var unspscCode = result.content[0].unspscCode;
                punchOutOrderMessage.itemIn[i].itemDetail.classification[0].domain = "UNSPSC";
                punchOutOrderMessage.itemIn[i].itemDetail.classification[0].value = unspscCode;
            }
        } catch(err) {
            console.log("Exception calling Product Search: " + err);
        }
    }
}

Ariba Customizations

// Ariba-specific customizations
if (punchOutSetupRequestCXML.payloadID.indexOf("ariba\\.com") !== -1) {
    if (punchOutOrderMessage && punchOutOrderMessage.itemIn && punchOutOrderMessage.itemIn.length > 0) {
        for (var i = 0; i < punchOutOrderMessage.itemIn.length; i++) {
            // Add UNSPSC classification
            if (punchOutOrderMessage.itemIn[i].itemDetail.classification && 
                punchOutOrderMessage.itemIn[i].itemDetail.classification.length > 0) {
                
                punchOutOrderMessage.itemIn[i].itemDetail.classification[0].domain = 'UNSPSC';
                
                // Find product to get UNSPSC code
                var pimProduct = findProductByCode(
                    punchOutOrderMessage.itemIn[i].itemID.supplierPartID.value
                );
                
                if (pimProduct && pimProduct.vendorProductGroupCode) {
                    punchOutOrderMessage.itemIn[i].itemDetail.classification[0].value = 
                        pimProduct.vendorProductGroupCode;
                }
            }
        }
    }
}

5. Address Mapping


5.1 Mapping to ERP Addresses

When processing orders from punchout systems, you often need to map the address IDs from the procurement system to internal ERP address codes.

/*
Script designed to run during punchout OrderRequest to allow the system to map an addressID
coming from a punchout system to an ERP addressID stored in Kodaris.

Vars in scope:
- erpAddressCode          - Should be set with the ERP address code to use
- punchoutAddressCode     - Contains the punchout address ID from the procurement system
- isShipping              - Boolean true if this is a shipping address
- isBilling               - Boolean true if this is a billing address
- customer                - The customer object for accessing customer data and settings
- companyCode             - The company code of the current punchout customer
- punchoutSystem          - The external punchout system making the call
*/

if (punchoutAddressCode && companyCode) {
    // Look up address mapping using company address search
    var searchParams = {
        "filterFields": [
            {
                "name": "companyCode",
                "operation": "IS",
                "value": companyCode
            },
            {
                "name": "user2",  // Field where punchout address IDs are stored
                "operation": "IS",
                "value": punchoutAddressCode
            }
        ]
    };
    
    var result = scriptServiceUtils.runAPIMethod(
        'POST', 
        '/api/system/company/address/search', 
        null, 
        null, 
        searchParams
    );

    // If found, use the ERP address code from the extra2 field
    if (result.totalElements >= 1 && result.content[0].extra2) {
        erpAddressCode = result.content[0].extra2;
    } else {
        // Allow to continue, system will try to find it using the original code
        erpAddressCode = null;
    }
}

This script allows you to map address IDs from procurement systems to internal ERP address codes, ensuring that orders are processed with the correct shipping and billing information.

6. XSLT Transformations


6.1 XSLT Overview

XSLT (Extensible Stylesheet Language Transformations) is a powerful technology for transforming XML documents into other formats or restructured XML. In the context of punchout, XSLT can be used to:

  • Transform non-standard cXML into standard format
  • Modify XML structure to meet specific procurement system requirements
  • Add or remove elements and attributes based on business rules
  • Format data to meet specific customer requirements

The Kodaris punchout implementation supports XSLT transformations specifically for order requests:

  • Incoming Order Requests: Transform PunchOutOrderRequest XML before processing using the punchOutOrderRequestXSLT company setting

Note: XSLT transformations are particularly useful when interceptors alone are not sufficient for complex XML restructuring, or when the transformation logic needs to be externalized from code.

6.2 Order Request Transformations

Order request transformations allow you to modify incoming PunchOutOrderRequest XML before it's processed by the system.

Configuring Order Request Transformations

Order request transformations can be configured using the punchOutOrderRequestXSLT company-specific setting.

<?xml version="1.0" encoding="UTF-8"?>
<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
   <!-- Copy all -->
   <xsl:template match="@*|node()">
      <xsl:copy>
         <xsl:apply-templates select="@*|node()" />
      </xsl:copy>
   </xsl:template>
   <!-- End -->
   
   <!-- Replace ShipTo addressID by Extrinsic/SpanNumber-->
   <xsl:variable name="shipToAddressCode">
      <xsl:for-each select="cXML/Request/OrderRequest/OrderRequestHeader/Extrinsic">
         <xsl:if test="@name='SpanNumber'">
            <xsl:value-of select="." />
         </xsl:if>
      </xsl:for-each>
   </xsl:variable>
   <xsl:template match="ShipTo/Address/@addressID">
      <xsl:attribute name="addressID">
         <xsl:value-of select="$shipToAddressCode" />
      </xsl:attribute>
   </xsl:template>
   <!-- End -->
</xsl:transform>

Explanation: This real-world example demonstrates a practical XSLT transformation that:

  1. Uses a standard identity template to copy all XML content by default
  2. Creates a variable shipToAddressCode that extracts the value from an Extrinsic element with name="SpanNumber"
  3. Replaces the addressID attribute in ShipTo/Address with this extracted value

This transformation is useful when the procurement system sends the actual shipping address code in an Extrinsic field rather than in the standard addressID attribute, allowing the system to map addresses correctly.

7. Complete Examples


7.1 Ariba Customizations

Ariba is one of the most common punchout providers and often requires specific customizations.

Typical Coupa Customizations

  • XML formatting: Removing standalone attribute, changing status text
  • UNSPSC classification: Adding proper classification domains and codes
  • Authentication: Handling user mapping for multi-location customers

Implementation Example

// punchOutSetupRequestSuccessResponse.js for Ariba
if (xmlRequest.match("ariba\\.com")) {
    // Remove standalone attribute from xml header
    xmlResponse = xmlResponse.replace(" standalone=\"yes\"", "");

    // Change success text status code from OK to Accepted
    xmlResponse = xmlResponse.replace("text=\"OK\"", "text=\"Accepted\"");
    
    // Other Ariba-specific customizations...
}

// cartPunchOutOrderMessage.js for Ariba
if (punchOutSetupRequestCXML.payloadID.indexOf("ariba\\.com") !== -1) {
    if (punchOutOrderMessage && punchOutOrderMessage.itemIn && punchOutOrderMessage.itemIn.length > 0) {
        for (var i = 0; i < punchOutOrderMessage.itemIn.length; i++) {
            // Customize UNSPSC classification
            if (punchOutOrderMessage.itemIn[i].itemDetail.classification && 
                punchOutOrderMessage.itemIn[i].itemDetail.classification.length > 0) {
                
                punchOutOrderMessage.itemIn[i].itemDetail.classification[0].domain = 'UNSPSC';
                
                // Get product info to set classification
                var pimProduct = findProductByCode(
                    punchOutOrderMessage.itemIn[i].itemID.supplierPartID.value
                );
                
                if (pimProduct && pimProduct.vendorProductGroupCode) {
                    punchOutOrderMessage.itemIn[i].itemDetail.classification[0].value = 
                        pimProduct.vendorProductGroupCode;
                }
            }
        }
    }
}

7.2 Coupa Customizations

Coupa is another popular punchout provider with its own set of requirements.

Typical Coupa Customizations

  • Unit of Measure standardization (e.g., "each" to "EA")
  • UNSPSC classification requirements
  • Custom extrinsic fields for additional product attributes

Implementation Example

// cartPunchOutOrderMessage.js for Coupa
if (punchOutSetupRequestCXML.payloadID.indexOf("coupa") !== -1) {
    for (var i=0; i < punchOutOrderMessage.itemIn.length; i++) {
        // Normalize unit of measure
        if ("each" === punchOutOrderMessage.itemIn[i].itemDetail.unitOfMeasure.toLowerCase()) {
            punchOutOrderMessage.itemIn[i].itemDetail.unitOfMeasure = "EA";
        }
        
        // Add UNSPSC classification
        var productCode = punchOutOrderMessage.itemIn[i].itemID.supplierPartID.value;
        try {
            var result = findProductByCode(productCode);
            
            if(result && result.unspscCode) {
                punchOutOrderMessage.itemIn[i].itemDetail.classification[0].domain = "UNSPSC";
                punchOutOrderMessage.itemIn[i].itemDetail.classification[0].value = result.unspscCode;
            }
        } catch(err) {
            console.log("Exception processing product: " + err);
        }
    }
}

Best Practices for Customization

  • Always test customizations with each procurement system
  • Log all actions for easier debugging
  • Use try/catch blocks to handle exceptions gracefully
  • Document the purpose of each customization
  • Keep provider-specific code separate and identifiable
  • Consider using feature flags to enable/disable specific customizations
In this article