· John Williams · Tutorials · 5 min read
How to do orchestration in Kong
One of the much awaited feature in Kong which was released in 3.10 is the Request Callout, which will help the Kong API management platform to perform orchestration/aggregation of API calls instead of teams exploring custom plugins

Pre-requisites
- Kong Konnect / any Kong deployed
- Minikube/any K8s Cluster (with Helm)
- Enterprise License
Aggregation vs Orchestration
One of the major functionalities required in an API Gateway is the ability to support injection of additional parameters based on a sideways call independent of the upstream/backend application being proxied. This has been supported by number of other gateway platforms and this feature came into Kong Gateways with the release of version 3.10
There are two ways how the data could be injected or massaged based on the requirements,
Aggregation : Aggregation refers to the process of collecting and combining data from multiple independent sources into a single, unified response. This reduces complexity for the client by handling multiple backend calls in one place, rather than forcing the client to make separate requests.
Orchestration : Orchestration is the coordinated management of a workflow across multiple services or APIs, where steps are executed in a specific sequence, often with dependencies, transformations, or conditional logic.
Kong Plugins
With the release of Kong 3.10, you have a new plugin Request Callout which allows you to insert arbitrary API calls before proxying a request to the upstream service and/or even call multiple APIs and aggregate the response. Let’s see this in action. For each of the example you would need three things - a service, a route and a plugin configured for request-callout and you can import the declarative yml file given in this link to add this setup into your gateway.
deck gateway sync --konnect-control-plane-name $CONTROL_PLANE_NAME --konnect-addr https://eu.api.konghq.com --konnect-token $KONNECT_TOKEN kong.yml
Make sure your gateway is up and running curl http://localhost:8000
Aggregation
In this scenario Kong will aggregate the response from two endpoints and return a combined response based on the structure of the json you require. You define two callouts and where the request should be sent out and how the response should be handled and decode: true is to to handle the json response from the URL. Finally how to handle the upstream call, here you are using the lua scripts to skip the backend call and to send back the aggregated response to the client calling the API. You are forming the final json response based on the data that you get from the two callouts made earlier.
Plugin Config
- name: request-callout
enabled: true
config:
callouts:
- name: c1
request:
method: GET
url: http://httpbin.org/uuid
response:
body:
decode: true
- name: c2
request:
method: GET
url: http://httpbin.org/anything
response:
body:
decode: true
upstream:
by_lua: kong.response.exit(200, { uuid = kong.ctx.shared.callouts.c1.response.body.uuid,
origin = kong.ctx.shared.callouts.c2.response.body.url})
protocols:
- grpc
- grpcs
- http
- https
route: callout-aggregate-route
service: callout-aggregateOutput
curl http://localhost:8000/callout-aggregate
{"origin":"http://httpbin.org/anything","uuid":"3d545847-6500-4c78-bc32-75996d23e3f6"}Orchestration
In this scenario Kong will call one endpoint after another and uses an output parameter from the first call to make the second call. First it will get the user details and fetch the userId details from the response and then fetch all the posts by that user and finally appends both in the response to you.
Plugin Config
- name: request-callout
enabled: true
config:
callouts:
- name: c1
request:
method: GET
url: https://jsonplaceholder.typicode.com/users/1
response:
body:
decode: true
- name: c2
request:
method: GET
url: https://jsonplaceholder.typicode.com/posts
query:
custom:
userId: $(callouts.c1.response.body.id)
response:
body:
decode: true
upstream:
by_lua: |
local user = kong.ctx.shared.callouts.c1.response.body
local posts = kong.ctx.shared.callouts.c2.response.body
-- Filter posts (e.g., only those with user matching)
local filtered_posts = {}
for _, post in ipairs(posts) do
if post.userId == user.id then
table.insert(filtered_posts, post)
end
end
local combined = {
user = user,
posts = filtered_posts
}
kong.response.set_header("Content-Type", "application/json")
kong.response.exit(200, combined)
protocols:
- http
- https
route: callout-orchestrate-route
service: callout-orchestrateOutput
curl http://localhost:8000/callout-orchestrate
{{"user":{"address":{"suite":"Apt. 556","city":"Gwenborough","zipcode":"92998-3874","street":"Kulas Light","geo":{"lng":"81.1496","lat":"-37.3159"}},"company":{"catchPhrase":"Multi-layered client-server neural-net","bs":"harness real-time e-markets","name":"Romaguera-Crona"},"website":"hildegard.org","id":1,"phone":"1-770-736-8031 x56442","email":"Sincere@april.biz","username":"Bret","name":"Leanne Graham"},"posts":[{"title":"sunt aut facere repellat provident occaecati excepturi optio reprehenderit","userId":1,"body":"quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto","id":1},{"title":"qui est esse","userId":1,"body":"est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla","id":2}]}}Auth Header Injection
In this scenario Kong will call token endpoint before calling the upstream and inject a new Authorization header into the upstream call. Here you can see the header is injected in the httpbin response under the header section.
Plugin Config
- name: request-callout
enabled: true
config:
callouts:
- name: auth
request:
url: "{vault://kcsvault/TOKEN_URL}"
method: POST
body:
custom:
grant_type: client_credentials
client_id: "{vault://kcsvault/CLIENT_ID}"
client_secret: "{vault://kcsvault/CLIENT_SECRET}"
audience: https://api.apiprimer.com
response:
body:
store: true
decode: true
upstream:
headers:
custom:
Authorization: Bearer $(callouts.auth.response.body.access_token)
route: callout-auth-route
service: callout-authOutput
curl http://localhost:8000/callout-auth
{
"args": {},
"headers": {
"Accept": "*/*",
"Authorization": "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InFXNXJtQ2tXZGpyZ1BNOGdJUVdzTCJ9.eyJpc3MiOiJodHRwczovL2Rldi14ZXJ1cmt6cC51cy5hdXRoMC5jb20vIiwic3ViIjoiell0M2E0WVJ2QVh0aHhZV01xV1RsYkR5S296WmM1WmpAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vYXBpLmFwaXByaW1lci5jb20iLCJpYXQiOjE3NTk1MjIxNjIsImV4cCI6MTc1OTYwODU2MiwiZ3R5IjoiY2xpZW50LWNyZWRlbnRpYWxzIiwiYXpwIjoiell0M2E0WVJ2QVh0aHhZV01xV1RsYkR5S296WmM1WmoifQ.cXuLRXV-7k2Z18BVFE9HWCoEoWUgvzUBFPjS1RwtUh-x3ZZhxkQUvDXhHRc5LcjVYe7HERTKJNrF-hoXtQRPmkVvFfmFdFA7DRJdl8F9gQGVcMRXcsv3ki0Q2UsMR--7JNahJ--9OHFjE7UlveOOe7iA145lYoQYb9LBnYWtdU2c55eVRdHL0pQWCLnGm429BMeRAaGkL7DUiIhmnEJeXGn9KEmFxwcZwYcYf13cmkizI7QT8uKPjaOdi_euUe-0YQeOECoKp5b61CbJxua-98gs5TYQGX2uTLehqc7pAI8TqKbdRUOIMSlFzZ-wCWsy4qC2tWy7TXczRID9v7MDQg",
"Host": "localhost",
"User-Agent": "curl/8.7.1",
"X-Amzn-Trace-Id": "Root=1-68e02d72-7a012a760392f0f01fc44325",
"X-Forwarded-Host": "localhost",
"X-Forwarded-Path": "/callout-auth",
"X-Forwarded-Prefix": "/callout-auth",
"X-Kong-Request-Id": "82b55227ed0f8a1f9df84673a2f94d02"
},
"url": "http://localhost/get"
}This plugin will be of great interest in the community where there are lot of systems involved and adding an additional simple logics in the Gateway. But be mindful about the usage of this plugin, as the latency caused by the external calls may start blocking the gateway if used too much. For light weight cases this is surely a great tool as it can replace the custom plugin generation and maintenance of that code.
I have also seen customers looking for such functionality who are coming from an Apigee or other platforms where this was supported, probably this would be an additional checked box when comparing between different gateway platforms.


