Composite Endpoints
When one call needs to become several, composite endpoints chain existing proxy endpoints into a sequence. Each step calls a named proxy endpoint, receives its response, and can hand extracted values to the steps that follow. Your caller sends one request and receives one combined result; steps execute in order, and if any step fails, execution stops there.
WARNING
Composite endpoints do not provide automatic rollback: steps that complete before a failure remain committed. It pays to design for this up front, with idempotent steps and a clean-up procedure for partial failures.
INFO
Each step references an existing proxy endpoint by name. Configure your proxy endpoints first before building composites on top of them.
Configuration
Create endpoints/Proxy/{CompositeName}/entity.json:
{
"Type": "Composite",
"Url": "http://internal-service/api",
"Methods": ["POST"],
"CompositeConfig": {
"Name": "CreateOrder",
"Description": "Creates order lines then the order header",
"Steps": [
{
"Name": "CreateOrderLines",
"Endpoint": "OrderLine",
"Method": "POST",
"IsArray": true,
"ArrayProperty": "Lines",
"TemplateTransformations": {
"TransactionKey": "$guid"
}
},
{
"Name": "CreateOrderHeader",
"Endpoint": "OrderHeader",
"Method": "POST",
"SourceProperty": "Header",
"TemplateTransformations": {
"TransactionKey": "$prev.CreateOrderLines.0.d.TransactionKey"
}
}
]
}
}Top-level properties
| Property | Required | Type | Description |
|---|---|---|---|
Type |
Yes | string | Must be "Composite" |
Url |
Yes | string | Base URL used for all steps |
Methods |
Yes | array | Supported HTTP methods. Composite endpoints typically accept ["POST"] only |
CompositeConfig |
Yes | object | Step definitions and operation metadata |
CompositeConfig properties
| Property | Required | Type | Description |
|---|---|---|---|
Name |
Yes | string | Identifier for this composite operation |
Description |
No | string | Human-readable description, included in OpenAPI docs |
Steps |
Yes | array | Ordered list of steps to execute |
Step properties
| Property | Required | Type | Description |
|---|---|---|---|
Name |
Yes | string | Unique step name, used in $prev references |
Endpoint |
Yes | string | Name of the proxy endpoint to call |
Method |
Yes | string | HTTP method to use for this step |
IsArray |
No | boolean | When true, iterates over an array and calls the endpoint once per item |
ArrayProperty |
Yes if IsArray | string | Property in the request body containing the array |
SourceProperty |
No | string | Extract this property from the request body and send it as the step's request body |
TemplateTransformations |
No | object | Values to inject or override in the request body before the step executes |
Template transformations
Use transformations to inject generated values or reference data from earlier steps:
| Template | Description |
|---|---|
$guid |
Generates a new GUID |
$requestid |
Uses the incoming request ID |
$prev.StepName.property |
References a property from a previous step's response |
$context.variable |
References a context variable |
Access nested and array results with dot notation:
{
"TemplateTransformations": {
"TransactionKey": "$prev.CreateOrderLines.0.d.TransactionKey",
"LineCount": "$prev.CreateOrderLines.length"
}
}Request and response format
Request:
POST /api/prod/composite/CreateOrder
Content-Type: application/json
Authorization: Bearer <token>
{
"Header": {
"CustomerCode": "CUST001",
"OrderDate": "2024-03-15",
"Reference": "PO-12345"
},
"Lines": [
{ "ProductCode": "PROD001", "Quantity": 5, "Price": 99.99 },
{ "ProductCode": "PROD002", "Quantity": 3, "Price": 149.99 }
]
}Success response:
{
"success": true,
"stepResults": {
"CreateOrderLines": [
{ "d": { "TransactionKey": "abc-123", "LineNumber": 1, "ProductCode": "PROD001" } },
{ "d": { "TransactionKey": "abc-123", "LineNumber": 2, "ProductCode": "PROD002" } }
],
"CreateOrderHeader": {
"d": { "OrderNumber": "SO-001234", "TransactionKey": "abc-123", "Status": "Created" }
}
}
}Failure response:
{
"success": false,
"errorStep": "CreateOrderHeader",
"errorMessage": "Insufficient credit limit",
"errorDetail": "Customer credit limit exceeded",
"statusCode": 400,
"stepResults": {
"CreateOrderLines": [...]
}
}StepResults always includes results from steps that completed before the failure.
Example: multi-service operation
{
"Type": "Composite",
"Url": "http://erp-service/api",
"Methods": ["POST"],
"CompositeConfig": {
"Name": "ProcessOrder",
"Steps": [
{
"Name": "ValidateCustomer",
"Endpoint": "CustomerService",
"Method": "POST",
"SourceProperty": "Customer"
},
{
"Name": "CheckInventory",
"Endpoint": "InventoryService",
"Method": "POST",
"SourceProperty": "Items"
},
{
"Name": "CreateOrder",
"Endpoint": "OrderService",
"Method": "POST",
"TemplateTransformations": {
"CustomerId": "$prev.ValidateCustomer.id",
"AvailableItems": "$prev.CheckInventory.available"
}
}
]
}
}Troubleshooting
"Endpoint not found": The step's Endpoint value must match an existing proxy endpoint name exactly (case-sensitive).
Transformation resolves to null: Verify the $prev.StepName.property path against the actual response structure of the referenced step. Check array indices when referencing array results.
Timeouts: Total execution time includes all steps in sequence. If individual steps are slow, the composite timeout can be reached. Test each step individually first.
To increase log verbosity:
{
"Logging": {
"LogLevel": {
"PortwayApi.Classes.CompositeEndpointHandler": "Debug"
}
}
}
Portway