Product Implementation
The APEX Platform provides a standardized product flow that helps you surface your products to users. This guide explains how to implement the necessary components for successful integration.
Product Flow
When a tenant initiates a new product flow, the system goes through several steps to display content to the user:
-
Initiation - The tenant initiates the start request to the platform, which is used by all tenants to surface products.
-
Legal - The platform retrieves data from the product catalog for the specified product and checks for any legal requirements before proceeding.
-
Start - The platform communicates with your product by calling the Start endpoint based on your product's configured base URL.
-
Verify Inputs - If your Start endpoint indicates that user inputs are required, the platform displays your web component to collect data from the user, then sends this data to your Verify Inputs endpoint.
-
Quoting - The platform generates a quote for the product. If the user has the appropriate contract permissions, the flow continues; otherwise, the quote displays for user input.
-
Payment - After quote acceptance, the platform collects funds from the user through the specified payment method (debit order or credit card).
-
Process - Once the previous steps complete successfully, the platform calls your Process endpoint to surface the final purchased product as a web component.
Product API Implementation
Your product needs to implement specific endpoints to be compatible with the platform. The APEX Platform library provides predefined response types to help you get your product running quickly.
- All endpoints use POST method - Each endpoint receives a body payload and uses POST requests
- Authentication required - All endpoints require proper authentication headers (
x-ls-partyid
andx-authenticated-tenantid
) - Typed responses - Endpoints return specific domain objects, not generic JSON payloads
Start Endpoint
The Start endpoint initializes your product. The platform expects one of two responses:
ShowUIStartResponse
- When user input is requiredContinueStartResponse
- When no user input is needed
Response Type Definitions:
ShowUIStartResponse
- Used when user input is required:
{
"elementName": "string", // Required: Name of the custom web component
"elementUrl": "string", // Required: URL where the component is hosted
"attributes": { // Optional: Key-value pairs set as HTML attributes
"key1": "value1",
"key2": "value2"
},
"body": "string" // Optional: JSON string passed to the component
}
ContinueStartResponse
- Used when no user input is needed:
{
"productDescription": "string" // Required: Description of the product
}
Examples
When no user input is required:
- C#
- Java
- Python
public async override Task<ActionResult<StartResponse>> Start([FromBody] ProductFlowInstanceStartModel input)
{
return new ContinueStartResponse
{
ProductDescription = "PRODUCT NAME"
};
}
@Override
public CompletableFuture<ActionResult<StartResponse>> start(ProductFlowInstanceStartModel input) {
ContinueStartResponse response = new ContinueStartResponse();
response.setProductDescription("PRODUCT NAME");
return CompletableFuture.completedFuture(new ActionResult<>(response));
}
async def start(self, input: ProductFlowInstanceStartModel) -> ActionResult[StartResponse]:
return ActionResult(ContinueStartResponse(
product_description="PRODUCT NAME"
))
When user input is required:
- C#
- Java
- Python
public async override Task<ActionResult<StartResponse>> Start([FromBody] ProductFlowInstanceStartModel input)
{
return new ShowUIStartResponse
{
ElementName = INPUT_ELEMENT_NAME,
ElementUrl = INPUT_ELEMENT_URL
};
}
@Override
public CompletableFuture<ActionResult<StartResponse>> start(ProductFlowInstanceStartModel input) {
ShowUIStartResponse response = new ShowUIStartResponse();
response.setElementName(INPUT_ELEMENT_NAME);
response.setElementUrl(INPUT_ELEMENT_URL);
return CompletableFuture.completedFuture(new ActionResult<>(response));
}
async def start(self, input: ProductFlowInstanceStartModel) -> ActionResult[StartResponse]:
return ActionResult(ShowUIStartResponse(
element_name=INPUT_ELEMENT_NAME,
element_url=INPUT_ELEMENT_URL
))
Validate Inputs Endpoint
When a user completes inputs from a ShowUIStartResponse
, the inputs are posted to the Validate Inputs endpoint. You should validate the inputs and return an appropriate response. If validation fails, the platform will redisplay the inputs screen.
Example
- C#
- Java
- Python
public async override Task<ActionResult<ValidateInputResponse>> ValidateInputs([FromBody] ProductFlowInstanceInput<HelloWorldInput> input)
{
return new ValidateInputResponse()
{
Succesful = true
};
}
@Override
public CompletableFuture<ActionResult<ValidateInputResponse>> validateInputs(ProductFlowInstanceInput<HelloWorldInput> input) {
ValidateInputResponse response = new ValidateInputResponse();
response.setSuccessful(true);
return CompletableFuture.completedFuture(new ActionResult<>(response));
}
async def validate_inputs(self, input: ProductFlowInstanceInput[HelloWorldInput]) -> ActionResult[ValidateInputResponse]:
return ActionResult(ValidateInputResponse(
successful=True
))
Process Endpoint
After successfully completing all previous steps, the platform calls the Process endpoint on your product API. This endpoint must return either a ContinueProcessResponse
or a ShowUIProcessResponse
based on the data received.
Examples
Simple continuation response:
- C#
- Java
- Python
public async override Task<ActionResult<ProcessResponse>> Process([FromBody] ProductFlowInstanceProcessModel<HelloWorldInput> input)
{
return new ContinueProcessResponse();
}
@Override
public CompletableFuture<ActionResult<ProcessResponse>> process(ProductFlowInstanceProcessModel<HelloWorldInput> input) {
return CompletableFuture.completedFuture(new ActionResult<>(new ContinueProcessResponse()));
}
async def process(self, input: ProductFlowInstanceProcessModel[HelloWorldInput]) -> ActionResult[ProcessResponse]:
return ActionResult(ContinueProcessResponse())
Response with UI component:
- C#
- Java
- Python
public async override Task<ActionResult<ProcessResponse>> Process([FromBody] ProductFlowInstanceProcessModel<HelloWorldInput> input)
{
return new ShowUIProcessResponse
{
Attributes = { { "name", input.Input.Data.Name } },
ElementName = FINAL_ELEMENT_NAME,
ElementUrl = FINAL_ELEMENT_URL,
Body = ""
};
}
@Override
public CompletableFuture<ActionResult<ProcessResponse>> process(ProductFlowInstanceProcessModel<HelloWorldInput> input) {
ShowUIProcessResponse response = new ShowUIProcessResponse();
Map<String, String> attributes = new HashMap<>();
attributes.put("name", input.getInput().getData().getName());
response.setAttributes(attributes);
response.setElementName(FINAL_ELEMENT_NAME);
response.setElementUrl(FINAL_ELEMENT_URL);
response.setBody("");
return CompletableFuture.completedFuture(new ActionResult<>(response));
}
async def process(self, input: ProductFlowInstanceProcessModel[HelloWorldInput]) -> ActionResult[ProcessResponse]:
return ActionResult(ShowUIProcessResponse(
attributes={"name": input.input.data.name},
element_name=FINAL_ELEMENT_NAME,
element_url=FINAL_ELEMENT_URL,
body=""
))
Handle Error Endpoint
When an error occurs during the product flow process, the platform calls your product's HandleError endpoint. You can choose to display a custom error UI or use the platform's default error message.
Examples
Using platform's default error message:
- C#
- Java
- Python
public async override Task<ActionResult<HandleErrorResponse>> HandleError([FromBody] ProductFlowInstanceHandleErrorModel errorInput)
{
return new ContinueHandleErrorResponse();
}
@Override
public CompletableFuture<ActionResult<HandleErrorResponse>> handleError(ProductFlowInstanceHandleErrorModel errorInput) {
return CompletableFuture.completedFuture(new ActionResult<>(new ContinueHandleErrorResponse()));
}
async def handle_error(self, error_input: ProductFlowInstanceHandleErrorModel) -> ActionResult[HandleErrorResponse]:
return ActionResult(ContinueHandleErrorResponse())
Displaying custom error UI:
- C#
- Java
- Python
public async override Task<ActionResult<HandleErrorResponse>> HandleError([FromBody] ProductFlowInstanceHandleErrorModel errorInput)
{
return new ShowUIHandleErrorResponse
{
ElementName = ERROR_ELEMENT_NAME,
ElementUrl = ERROR_ELEMENT_URL,
Attributes = { { "error-status", errorInput.ProductFlowInstanceStatus }},
Body = ""
};
}
@Override
public CompletableFuture<ActionResult<HandleErrorResponse>> handleError(ProductFlowInstanceHandleErrorModel errorInput) {
ShowUIHandleErrorResponse response = new ShowUIHandleErrorResponse();
Map<String, String> attributes = new HashMap<>();
attributes.put("error-status", errorInput.getProductFlowInstanceStatus());
response.setAttributes(attributes);
response.setElementName(ERROR_ELEMENT_NAME);
response.setElementUrl(ERROR_ELEMENT_URL);
response.setBody("");
return CompletableFuture.completedFuture(new ActionResult<>(response));
}
async def handle_error(self, error_input: ProductFlowInstanceHandleErrorModel) -> ActionResult[HandleErrorResponse]:
return ActionResult(ShowUIHandleErrorResponse(
element_name=ERROR_ELEMENT_NAME,
element_url=ERROR_ELEMENT_URL,
attributes={"error-status": error_input.product_flow_instance_status},
body=""
))
Attributes and Body Properties
When using ShowUI responses, you can customize the component rendering with:
- Body - A JSON string containing data to pass to your custom web component
- Attributes - Key-value pairs set as attributes for your custom web component
Usage in Product Lifecycle:
- Attributes: Set as HTML attributes on your web component (e.g.,
<custom-component custom-attr="value">
) - Body: Passed as complex data objects to your component's internal logic
- Both properties work together to provide data and configuration to your micro front-end components
Example
- C#
- Java
- Python
return new ShowUIProcessResponse
{
Attributes = { { "custom-attr", "value" } },
ElementName = "custom-component"
};
ShowUIProcessResponse response = new ShowUIProcessResponse();
Map<String, String> attributes = new HashMap<>();
attributes.put("custom-attr", "value");
response.setAttributes(attributes);
response.setElementName("custom-component");
return response;
return ShowUIProcessResponse(
attributes={"custom-attr": "value"},
element_name="custom-component"
)
This generates HTML like:
<custom-component custom-attr="value"></custom-component>
Micro Front Ends
Your product also needs to include Micro Front Ends as custom web components. The platform renders these components on the tenant side based on responses from your product API.
Inputs Micro Front End
The inputs micro front end collects additional information from users. It can consist of multiple pages and have its own web services to improve user experience.
The micro front end properties are populated by the API using the attributes property. The body of the custom web component can also pass complex data objects as a JSON string.
When input collection is complete, the component must raise a custom event to notify the product flow:
let inputComplete = new CustomEvent('capability-input-complete', {
detail: {
data: {}
},
bubbles: true,
composed: true
});
this.dispatchEvent(inputComplete);
Product Micro Front End
The final step in the product flow calls the Process endpoint of your API and passes any collected input data.
Your API can then set various attributes and complex data as the body of your custom web component, allowing you to render the final product to the client.
For a sample implementation using LitElement, refer to the samples project.
Domain Objects and Dependencies
C# Dependencies
C# implementations can use NuGet packages that provide the required domain objects and response types. These packages include all the necessary models like ProductFlowInstanceStartModel
, ShowUIStartResponse
, ContinueStartResponse
, etc.
Java and Python Dependencies
For Java and Python implementations, the required domain objects are defined inline in the code examples above. You can use these as templates to create your own domain classes:
Key Domain Objects:
Input Models:
ProductFlowInstanceStartModel
- Input model for Start endpointProductFlowInstanceInput<T>
- Input model for ValidateInputs endpointProductFlowInstanceProcessModel<T>
- Input model for Process endpointProductFlowInstanceHandleErrorModel
- Input model for HandleError endpoint
Response Models with JSON Schemas:
Start Endpoint Responses:
// ShowUIStartResponse
{
"elementName": "string",
"elementUrl": "string",
"attributes": { "key": "value" }, // Optional
"body": "string" // Optional
}
// ContinueStartResponse
{
"productDescription": "string"
}
ValidateInputs Endpoint Response:
// ValidateInputResponse
{
"successful": boolean,
"errorMessage": "string" // Optional, used when successful=false
}
Process Endpoint Responses:
// ShowUIProcessResponse
{
"elementName": "string",
"elementUrl": "string",
"attributes": { "key": "value" }, // Optional
"body": "string" // Optional
}
// ContinueProcessResponse
{
// Empty response indicating successful completion
}
HandleError Endpoint Responses:
// ShowUIHandleErrorResponse
{
"elementName": "string",
"elementUrl": "string",
"attributes": { "key": "value" }, // Optional
"body": "string" // Optional
}
// ContinueHandleErrorResponse
{
// Empty response using platform's default error handling
}
Refer to the code examples in each section for the complete class definitions and usage patterns.
Testing and Development
Local Testing
You can test your product implementation locally by:
- Sandbox Environment: Use APEX's sandbox environment for integration testing
- Mock Data: Create mock
ProductFlowInstanceStartModel
objects to test your endpoints - Unit Testing: Test each endpoint individually with sample payloads
- Integration Testing: Test the complete flow using the sandbox environment
Simulating Product Workflow
To simulate a complete product workflow:
- Start with a mock Start request to your endpoint
- If your endpoint returns
ShowUIStartResponse
, simulate user input collection - Test the ValidateInputs endpoint with the collected data
- Finally, test the Process endpoint to verify product delivery
- Test error scenarios using the HandleError endpoint
- Store the
productFlowInstanceId
to maintain state across requests - Implement proper error handling and logging
- Test thoroughly in sandbox before production deployment
- Use the provided code samples as starting templates
Authentication and Billing
Product Flow Authentication
Product Flow endpoints require authentication headers for API calls:
x-ls-partyid
- The party identifierx-authenticated-tenantid
- The authenticated tenant identifier
API Gateway and Product Flow Integration
The API Gateway and Product Flow work together in the following way:
- API Gateway: Provides authentication, rate limiting, and automatically starts a "user not present" product flow to log transactions for billing and usage tracking
- Product Flow: Can operate in two modes:
- User Present: Interactive flows with user input and UI components (standard product delivery)
- User Not Present: Background flows for transaction logging (triggered by API Gateway calls)
Billing Integration
- Interactive Product Flows: Handle billing through the standardized workflow during the "Payment" step where users complete payment via the UI
- API Gateway Transactions: Automatically create "user not present" product flows for transaction logging and billing purposes
- This ensures all API usage is properly tracked and billed regardless of whether it's an interactive product flow or a direct API call