API Extensions
Extend the behavior of an API with your business logic.
The commercetools Composable Commerce APIs provide default data structures and behavior that is useful for many customers. However, each project has its own unique requirements. Similar to data structures that you can customize with Custom Types and Product Types, you can also add additional behavior. For behavior that should be executed within a short timeframe, you can use Subscriptions. For behavior that must be executed before the API call succeeds, you can use API Extensions.
Good use cases for API Extensions are: Validating the content of a Cart (for example no more than 8 crates of beverages can be ordered at once), calculating custom shipping costs, or adding mandatory items, like insurance, to a Cart.
An API Extension gets called after the processing of a create or update request of an API call, but before the result is persisted. The API Extension can validate the object, or apply additional updates to it.
You may host your API Extensions as you wish, but we provide integrations with serverless functions.
An API Extension affects the performance of the API it is extending. If it fails or takes a second longer to return a result, the whole API call fails or takes a second longer. Use Subscriptions instead of API Extensions when you can react to events asynchronously.
An API Extension is applied to API calls from all clients, including those provided by commercetools, like the Merchant Center.
API Extensions are available for the following resources:
A Project can have a maximum of 25
API Extensions. When you create, update, or delete API Extensions, it may take upto a minute for the changes to update. For more information, see Eventual Consistency.
This document explains the creation of an API Extension, its input, responses, conditional triggers and limits and error cases. You can also see the step-by-step tutorial on how to set up an API Extension.
Learn more about using API Extensions in our self-paced Extensibility overview module.
Representations
Extension
id String | Unique identifier of the Extension. |
version Int | Current version of the Extension. |
key String | User-defined unique identifier of the Extension. MinLength:2 MaxLength: 256 Pattern: ^[A-Za-z0-9_-]+$ |
destination | The configuration for the Extension, including its type, location and authentication details. |
triggers Array of ExtensionTrigger | Describes what triggers the Extension. |
timeoutInMs Int | Maximum time (in milliseconds) that the Extension can respond within.
If no timeout is provided, the default value is used for all types of Extensions.
The maximum value is 10000 ms (10 seconds) for 2000 |
createdAt | Date and time (UTC) the Extension was initially created. |
createdBy BETA | IDs and references that created the Extension. |
lastModifiedAt | Date and time (UTC) the Extension was last updated. |
lastModifiedBy BETA | IDs and references that last modified the Extension. |
ExtensionDraft
key String | User-defined unique identifier for the Extension. MinLength:2 MaxLength: 256 Pattern: ^[A-Za-z0-9_-]+$ |
destination | Defines where the Extension can be reached. |
triggers Array of ExtensionTrigger | Describes what triggers the Extension. |
timeoutInMs Int | Maximum time (in milliseconds) the Extension can respond within.
If no timeout is provided, the default value is used for all types of Extensions.
The maximum value is 10000 ms (10 seconds) for This limit can be increased per Project after we review the performance impact. Please contact the Composable Commerce support team and provide the Region, Project key, and use case. Default:2000 |
ExtensionPagedQueryResponse
PagedQueryResult with results
containing an array of Extension.
limit Int | Number of results requested. |
offset Int | Number of elements skipped. |
count Int | Actual number of results returned. |
total Int | Total number of results matching the query.
This number is an estimation that is not strongly consistent.
This field is returned by default.
For improved performance, calculating this field can be deactivated by using the query parameter |
results Array of Extension | Extensions matching the query. |
ExtensionResourceTypeId
Extensions are available for:
cart
Extension triggered for operations on Carts.
order
Extension triggered for operations on Orders.
payment
Extension triggered for operations on Payments.
customer
Extension triggered for operations on Customers.
quote-request
Extension triggered for operations on QuoteRequests.
staged-quote
Extension triggered for operations on StagedQuotes.
quote
Extension triggered for operations on Quotes.
business-unit
Extension triggered for operations on BusinessUnits.
shopping-list
Extension triggered for operations on ShoppingLists.
ExtensionAction
An Extension gets called during any of the following requests of an API call, but before the result is persisted.
Create
An Extension gets called during a Create request.
Update
An Extension gets called during an Update request.
ExtensionDestination
A destination contains the configuration for the Extension, including its type
, location and authentication details.
We believe deploying an API Extension on a Function-as-a-Service is a good practice. Azure Functions can be called with the HTTP
destination using AzureFunctionsAuthentication. For Google Cloud Functions, although the HTTP
destination can be used, we recommend using the dedicated GoogleCloudFunctionDestination
because invocations can be authorized through the Google Cloud Platform (GCP) Identity and Access Management (IAM). This is simpler than using the HTTPDestination
and having to provide an authorization token.
When retrieving a destination, secrets or access keys are partially hidden for security reasons.
HTTPDestination
We recommend an encrypted HTTPS
connection for production setups. However, we also accept unencrypted HTTP
connections for development purposes. HTTP redirects will not be followed and cache headers will be ignored.
type String | "HTTP" |
url String | URL to the target destination. |
authentication | Authentication methods (such as |
GoogleCloudFunctionDestination
Google Cloud Functions limit the size of the payload. The exact limit is determined by the function's version.
For GoogleCloudFunction destinations, you need to grant permissions to the extensions@commercetools-platform.iam.gserviceaccount.com
service account to invoke your function. If your function's version is 1st gen, grant the service account the IAM role Cloud Functions Invoker
. For version 2nd gen, assign the IAM role Cloud Run Invoker
using the Cloud Run console.
type String | "GoogleCloudFunction" |
url String | URL to the target function. |
commercetools Projects hosted on Google Cloud can benefit from additional security by enabling Google Cloud's VPC Service Controls. This ensures that Google Cloud Functions can only be accessed from the commercetools Google Cloud infrastructure.
To configure VPC Service Controls, specify the commercetools GCP project number as the source of your ingress policy rule. Contact your Customer Success Manager to obtain the required commercetools GCP project number.
AWSLambdaDestination
AWS Lambda limits the size of the payload to 6 MB. The limit also applies if the Lambda function is invoked by the API Gateway.
Do not use AWS Lambda if you anticipate that your API Extension will receive a JSON input exceeding 6 MB.
We recommend creating an Identify and Access Management (IAM) user with an accessKey
and accessSecret
pair, specifically for each Extension that only has the lambda:InvokeFunction
permission on this function.
type String | "AWSLambda" |
arn String | Amazon Resource Name (ARN) of the Lambda function in the format |
accessKey String | Partially hidden on retrieval for security reasons. |
accessSecret String | Partially hidden on retrieval for security reasons. |
HTTPDestinationAuthentication
You can secure your HTTP destinations by setting an Authorization
header using AuthorizationHeaderAuthentication. For calling Azure functions specifically, use AzureFunctionsAuthentication to specify the function key.
AzureFunctionsAuthentication
To protect your Azure Function, set its authLevel
to function
and provide the function's key to be used inside the x-functions-key
header. For more information, see the Azure Functions documentation.
To protect the secret key from being exposed, remove the code parameter and secret key from the URL. For example, use https://foo.azurewebsites.net/api/bar
instead of
https://foo.azurewebsites.net/api/bar?code=secret
.
key String | Partially hidden on retrieval for security reasons. |
type String | "AzureFunctions" |
ExtensionTrigger
resourceTypeId |
|
actions Array of ExtensionAction |
|
condition String | Valid predicate that controls the conditions under which the API Extension is called. The Extension is not triggered when the specified condition is not fulfilled. |
Get Extension
Get Extension by ID
manage_extensions:{projectKey}
region String | Region in which the Project is hosted. |
projectKey String |
|
id String |
|
application/json
curl --get https://api.{region}.commercetools.com/{projectKey}/extensions/{id} -i \--header 'Authorization: Bearer ${BEARER_TOKEN}'
{"id": "8062243c-46fc-40b5-88a4-75e2216aef75","version": 1,"createdAt": "2017-01-25T14:14:22.417Z","lastModifiedAt": "2017-01-25T14:14:22.417Z","destination": {"type": "HTTP","url": "https://example.azurewebsites.net/api/extension","authentication": {"type": "AzureFunctions","key": "****code"}},"triggers": [{"resourceTypeId": "cart","actions": ["Create", "Update"]}],"key": "my-extension"}
Get Extension by Key
manage_extensions:{projectKey}
region String | Region in which the Project is hosted. |
projectKey String |
|
key String |
|
application/json
curl --get https://api.{region}.commercetools.com/{projectKey}/extensions/key={key} -i \--header 'Authorization: Bearer ${BEARER_TOKEN}'
{"id": "8062243c-46fc-40b5-88a4-75e2216aef75","version": 1,"createdAt": "2017-01-25T14:14:22.417Z","lastModifiedAt": "2017-01-25T14:14:22.417Z","destination": {"type": "HTTP","url": "https://example.azurewebsites.net/api/extension","authentication": {"type": "AzureFunctions","key": "****code"}},"triggers": [{"resourceTypeId": "cart","actions": ["Create", "Update"]}],"key": "my-extension"}
Query Extensions
manage_extensions:{projectKey}
region String | Region in which the Project is hosted. |
projectKey String |
|
where | The parameter can be passed multiple times. |
/^var[.][a-zA-Z0-9]+$/ Any string parameter matching this regular expression | Predicate parameter values. The parameter can be passed multiple times. |
sort | The parameter can be passed multiple times. |
limit Int | Number of results requested. |
offset Int | Number of elements skipped. |
withTotal Boolean | Controls the calculation of the total number of query results. Set to Default: true |
application/json
curl --get https://api.{region}.commercetools.com/{projectKey}/extensions -i \--header 'Authorization: Bearer ${BEARER_TOKEN}'
{"limit": 20,"offset": 0,"count": 1,"total": 1,"results": [{"id": "8062243c-46fc-40b5-88a4-75e2216aef75","version": 1,"createdAt": "2017-01-25T14:14:22.417Z","lastModifiedAt": "2017-01-25T14:14:22.417Z","destination": {"type": "HTTP","url": "https://example.org/extension"},"triggers": [{"resourceTypeId": "cart","actions": ["Create"]}]}]}
Check if Extension exists
Check if Extension exists by ID
Checks if an Extension exists for a given id
. Returns a 200 OK
status if the Extension exists or a 404 Not Found
otherwise.
manage_extensions:{projectKey}
region String | Region in which the Project is hosted. |
projectKey String |
|
id String |
|
curl --head https://api.{region}.commercetools.com/{projectKey}/extensions/{id} -i \--header 'Authorization: Bearer ${BEARER_TOKEN}'
Check if Extension exists by Key
Checks if an Extension exists for a given key
. Returns a 200 OK
status if the Extension exists or a 404 Not Found
otherwise.
manage_extensions:{projectKey}
region String | Region in which the Project is hosted. |
projectKey String |
|
key String |
|
curl --head https://api.{region}.commercetools.com/{projectKey}/extensions/key={key} -i \--header 'Authorization: Bearer ${BEARER_TOKEN}'
Check if Extension exists by Query Predicate
Checks if an Extension exists for a given Query Predicate. Returns a 200 OK
status if any Extensions match the Query Predicate or a 404 Not Found
otherwise.
manage_extensions:{projectKey}
region String | Region in which the Project is hosted. |
projectKey String |
|
where |
curl --head https://api.{region}.commercetools.com/{projectKey}/extensions -i \--header 'Authorization: Bearer ${BEARER_TOKEN}'
Create Extension
manage_extensions:{projectKey}
region String | Region in which the Project is hosted. |
projectKey String |
|
application/json
application/json
curl https://api.{region}.commercetools.com/{projectKey}/extensions -i \--header 'Authorization: Bearer ${BEARER_TOKEN}' \--header 'Content-Type: application/json' \--data-binary @- << DATA{"destination" : {"type" : "HTTP","url" : "https://example.azurewebsites.net/api/extension","authentication" : {"type" : "AzureFunctions","key" : "some-azure-function-code"}},"triggers" : [ {"resourceTypeId" : "cart","actions" : [ "Create", "Update" ]} ],"key" : "my-extension"}DATA
{"id": "8062243c-46fc-40b5-88a4-75e2216aef75","version": 1,"createdAt": "2017-01-25T14:14:22.417Z","lastModifiedAt": "2017-01-25T14:14:22.417Z","destination": {"type": "HTTP","url": "https://example.azurewebsites.net/api/extension","authentication": {"type": "AzureFunctions","key": "****code"}},"triggers": [{"resourceTypeId": "cart","actions": ["Create", "Update"]}],"key": "my-extension"}
Update Extension
Update Extension by ID
manage_extensions:{projectKey}
region String | Region in which the Project is hosted. |
projectKey String |
|
id String |
|
application/json
version Int | Expected version of the Extension on which the changes should be applied. If the expected version does not match the actual version, a ConcurrentModification error will be returned. |
actions Array of ExtensionUpdateAction | Update actions to be performed on the Extension. |
application/json
curl https://api.{region}.commercetools.com/{projectKey}/extensions/{id} -i \--header 'Authorization: Bearer ${BEARER_TOKEN}' \--header 'Content-Type: application/json' \--data-binary @- << DATA{"version" : 1,"actions" : [ {"action" : "setKey","key" : "my-new-extension-key"} ]}DATA
{"id": "8062243c-46fc-40b5-88a4-75e2216aef75","version": 2,"createdAt": "2017-01-25T14:14:22.417Z","lastModifiedAt": "2024-08-06T13:49:48.511Z","destination": {"type": "HTTP","url": "https://example.azurewebsites.net/api/extension","authentication": {"type": "AzureFunctions","key": "****code"}},"triggers": [{"resourceTypeId": "cart","actions": ["Create", "Update"]}],"key": "my-new-extension-key"}
Update Extension by Key
manage_extensions:{projectKey}
region String | Region in which the Project is hosted. |
projectKey String |
|
key String |
|
application/json
version Int | Expected version of the Extension on which the changes should be applied. If the expected version does not match the actual version, a ConcurrentModification error will be returned. |
actions Array of ExtensionUpdateAction | Update actions to be performed on the Extension. |
application/json
curl https://api.{region}.commercetools.com/{projectKey}/extensions/key={key} -i \--header 'Authorization: Bearer ${BEARER_TOKEN}' \--header 'Content-Type: application/json' \--data-binary @- << DATA{"version" : 1,"actions" : [ {"action" : "setKey","key" : "my-new-extension-key"} ]}DATA
{"id": "8062243c-46fc-40b5-88a4-75e2216aef75","version": 2,"createdAt": "2017-01-25T14:14:22.417Z","lastModifiedAt": "2024-08-06T13:49:48.511Z","destination": {"type": "HTTP","url": "https://example.azurewebsites.net/api/extension","authentication": {"type": "AzureFunctions","key": "****code"}},"triggers": [{"resourceTypeId": "cart","actions": ["Create", "Update"]}],"key": "my-new-extension-key"}
Update actions
Set Key
action String | "setKey" |
key String | Value to set. If empty, any existing value will be removed. MinLength:2 MaxLength: 256 Pattern: ^[A-Za-z0-9_-]+$ |
{"action": "setKey","key": "keyString"}
Change Triggers
action String | "changeTriggers" |
triggers Array of ExtensionTrigger | New value to set. Must not be empty. |
{"action": "changeTriggers","triggers": [{"resourceTypeId": "cart","actions": ["Create", "Update"],"condition": "field is defined and field has changed"}]}
Change Destination
action String | "changeDestination" |
destination | New value to set. Must not be empty. |
{"action": "changeDestination","destination": {"type": "HTTP","url": "URL-String","authentication": {"type": "AuthorizationHeader","headerValue": "valueString"}}}
Set TimeoutInMs
action String | "setTimeoutInMs" |
timeoutInMs Int | Value to set. If not defined, the maximum value is used.
If no timeout is provided, the default value is used for all types of Extensions.
The maximum value is 10000 ms (10 seconds) for This limit can be increased per Project after we review the performance impact. Please contact the Composable Commerce support team and provide the Region, Project key, and use case. Default:2000 |
Delete Extension
Delete Extension by ID
manage_extensions:{projectKey}
region String | Region in which the Project is hosted. |
projectKey String |
|
id String |
|
version Int | Last seen version of the resource. |
application/json
curl -X DELETE https://api.{region}.commercetools.com/{projectKey}/extensions/{id}?version={version} -i \--header 'Authorization: Bearer ${BEARER_TOKEN}'
{"id": "8062243c-46fc-40b5-88a4-75e2216aef75","version": 1,"createdAt": "2017-01-25T14:14:22.417Z","lastModifiedAt": "2017-01-25T14:14:22.417Z","destination": {"type": "HTTP","url": "https://example.azurewebsites.net/api/extension","authentication": {"type": "AzureFunctions","key": "****code"}},"triggers": [{"resourceTypeId": "cart","actions": ["Create", "Update"]}],"key": "my-extension"}
Delete Extension by Key
manage_extensions:{projectKey}
region String | Region in which the Project is hosted. |
projectKey String |
|
key String |
|
version Int | Last seen version of the resource. |
application/json
curl -X DELETE https://api.{region}.commercetools.com/{projectKey}/extensions/key={key}?version={version} -i \--header 'Authorization: Bearer ${BEARER_TOKEN}'
{"id": "8062243c-46fc-40b5-88a4-75e2216aef75","version": 1,"createdAt": "2017-01-25T14:14:22.417Z","lastModifiedAt": "2017-01-25T14:14:22.417Z","destination": {"type": "HTTP","url": "https://example.azurewebsites.net/api/extension","authentication": {"type": "AzureFunctions","key": "****code"}},"triggers": [{"resourceTypeId": "cart","actions": ["Create", "Update"]}],"key": "my-extension"}
API Extension within the flow of the API call
The API Extensions are called during the execution of a create or update request. The system first validates that the requested operation can be done, and applies the operation. For example, if an API call requests to add a Product to a Cart, the validation checks that the Product exists and is sold in the country of the Cart, and then adds it to the Cart. However, the result (for example the Cart) is not persisted yet. It is forwarded to the API Extensions, that can run their business logic. API Extension can respond on of three ways:
- If the Extension finds the result to be valid, and does not want to perform any changes, it can return a success. The system will persist the result.
- If the Extension finds that the result is not valid (for example the particular Customer may not purchase this Product), it can return a list of errors. The system will not persist the result and return the errors to the original caller of the API.
- If the Extension wants to perform additional changes, it can return a list of update actions (for example if it wants to add mandatory insurance to the Cart, it can return an
AddLineItem
update). The system will validate that the update actions are valid for the given resource type and will try to perform the updates. Should that fail, the original caller of the API will receive the errors. Otherwise, the original caller will receive the final result (for example the Cart including the mandatory insurance).
If an API Extension fails to respond properly
If an API Extension fails to respond properly, the whole API call will fail. If the API Extension did not return a result in time or could not be reached, the original API caller will receive a 504
Gateway Timeout
HTTP status code. If the API Extension returns a response, but it could not be parsed properly, the original API caller will receive a 502
Bad Gateway
HTTP status code.
In both cases, additional information on the cause of the failure is returned to the original API Client.
Please note that in terms of Service Level Agreement (SLA), a failure caused by an API Extension does not count as a failure of the commercetools Composable Commerce API. For example, if an API Extension for the Carts is down and causes downtime for your shop, the SLA won't cover that.
Multiple API Extensions in a single API call
If multiple API Extensions are triggered by an API call, they will be called in parallel. Their responses will be merged, but without a guaranteed order (for example, if two Extensions each return an error, their order in the error list is undefined. If two Extensions return updates, the order in which the updates are performed is undefined).
Responses are merged based on their priority or severity:
- A failure to respond properly by any API Extension will cause the whole request to fail with a
502
or504
. - Otherwise, if any API Extension finds that the result is not valid, the result will not be persisted and an error will be returned to the original caller of the API.
- Otherwise, if any API Extension returns updates, the result will be modified before it is returned to the original caller of the API.
API Extensions should operate on separate concerns. For example for a Cart, it is fine to have an Extension each for validating that a Customer is allowed to purchase age-restricted Products, calculating shipping costs, and adding mandatory insurance. A counter-example is two API Extensions changing the Price of a Line Item: One for adding extra costs for optional gift wrapping, the other reducing the Price if an externally defined Discount applies - If both Extensions want to change the same Line Item, one will overwrite the result of the other. For this use case, the whole Price calculation for Line Items should be performed inside a single Extension.
You also need to find a balance between clean separation and the increasing latency risk when calling many parallel Extensions.
Input
An HTTP
Extension will be called via HTTP POST request. An AWSLambda
Extension will be invoked, and the input is provided as the payload.
action |
|
resource | Expanded reference to the resource that triggered the Extension. |
The object will be persisted as-is if no Extension returns errors or updates. It is therefore identical to the result the user may see, with the exception of timestamps: the lastModifiedAt
field, and if it is a creation, the createdAt
field, do not contain the final values. All other fields will be persisted if they are not overridden by an update of an Extension.
Headers
For an HTTP
Extension, some headers will be set:
Content-Type
-application/json
X-Correlation-ID
- A correlation ID can be used to track a request. The same correlation ID will be returned to the original caller of the API.
In addition, the Authorization
or x-functions-key
header is set if configured.
Response
An Extension with an HTTP
destination must set a proper HTTP status code (200
or 201
for successful responses, 400
for validation failures). All other status codes will be treated as a failure to respond properly.
An Extension with an AWSLambda destination must return without errors (both for successful responses, and validation failures). Throwing an exception in Java, Python, C# or Go, or invoking the callback with an error in NodeJS, will be treated as a failure to respond properly.
The response can optionally contain a list of update actions to be applied to the resource. The possible update actions are limited to those available for the resource that triggered the API Extension.
Validation successful / No updates requested
The body should be completely empty or an empty list of requested updates.
An HTTP
Extension must set a 200
or 201
HTTP status code.
Validation failed
errors
- Array of Error - At least one error must be present.
An HTTP
Extension must set a 400
HTTP status code.
An AWSLambda
Extension must add the following field to the JSON:
responseType
- String -"FailedValidation"
Error
code
- String
Needs to be one of the defined error codes, for example,InvalidInput
orInvalidOperation
.message
- String
User-defined description of the error.localizedMessage
- LocalizedString - Optional
Localized user-defined description of the error. If a localized message is available for the user's locale settings, the Merchant Center displays it.extensionExtraInfo
- JSON Object - Optional
Any other information that should be returned to the API caller.
For more information, see errors from Extensions.
Updates requested
actions
- Array of update actions for the resourceType.
Up to100
update actions are allowed.
You can use all update actions for the particular resource:
- for
cart
you can use any Cart update action. - for
order
you can use any Order update action. - for
payment
you can use any Payment update action. - for
customer
you can use any Customer update action. - for
quote-request
you can use any Quote Request update action. - for
staged-quote
you can use any Staged Quote update action. - for
quote
you can use any Quote update action. - for
business-unit
you can use any Business Unit update action. - for
shopping-list
you can use any Shopping List update action.
An HTTP
Extension must set a 200
or 201
HTTP status code.
An AWSLambda
Extension must add the following field to the JSON:
responseType
- String -"UpdateRequest"
Conditional triggers
By default, API Extensions are always called during an update or create action on the resource they are configured for. However, in certain use cases, the Extension should only be triggered when certain conditions are met. Specifying conditional triggers can eliminate unnecessary API calls to your Extension. This eases the load on your service as well as improves overall performance.
You can define conditional statements using the predicate syntax in the condition
field of the ExtensionTrigger. The condition is evaluated during every create or update action to the configured resource, resulting in one of three outcomes:
- The condition evaluates to true and the API Extension is called.
- The condition evaluates to false, the API Extension is not called.
- The predicate syntax can not be evaluated and the entire API call fails.
If your predicate contains optional fields, use the is defined
operator to ensure that the field exists and has a non-null value. This will ensure that a missing field will not cause the evaluation of the condition to fail.
For further examples and use-cases, follow our tutorial on API Extensions.
Limits and error cases
An API Extension affects the performance and uptime of the API it is extending. Therefore, you should carefully consider the technology used to implement the API Extension and your hosting options.
We believe that deploying an Extension on a Function-as-a-Service is a good fit. You should get a very high up-time and auto-scaling at low costs. However, you should optimize your functions for good cold-start performance.
Independently of the technology choice, you should make sure that the network latency between the Region your Project is running on and your extension is as low as possible.
Time limits
If an API Extension is not responding fast, the whole API call is blocked. We, therefore, enforce the following limits per default:
- An API Extension must return a result within 2 seconds. This includes the network latency.
- For an API Extension with a
payment
trigger, the time limit can be raised to 10 seconds. - The commercetools Composable Commerce API must successfully establish a connection to the API Extension within 1 second.
The default timeout can be lowered.
We recommend that your API Extension returns results much faster, though. A good target is to respond within 50 ms.
Error cases
In any error case (such as no response within the time limit or a bad response like a 500
HTTP status code) the API call fails. The API Extension is not retried within an API call, but further API calls will try to reach the API Extension again.
The following errors codes are returned when an API Extension does not respond successfully: