API gateway batch processing with the loop node — per-item routing, validation, and error handling
How to process batch API events at the gateway layer using the Zerq loop node — per-item routing, validation, error handling, and a full audit trail.
- workflows
- how-to
- api-management
- platform-engineering
- fintech
Payment processors, ERP systems, and banking data feeds all share one design pattern: they batch events. A single webhook from Stripe or a bank notification service might carry 50 payment events in one HTTP request. A bulk FHIR resource submission might include 200 patient records. A B2B partner order feed might deliver 100 order updates in a single POST. The downstream handler has to iterate, fan out, and track per-item results — all logic that accumulates in your backend code over time and becomes a maintenance problem.
This is where API gateway batch processing belongs in the architecture. The gateway layer receives the batch, iterates over each item, validates it, routes it to the appropriate backend, and returns a structured per-item result — without the backend needing to implement any of that orchestration. When the gateway owns the batch loop, you get per-item audit visibility, per-event routing, per-item error isolation, and a single place to change the processing logic when the external format changes.
Most gateways cannot do this. They proxy the whole body to a single upstream endpoint and treat the array as an opaque payload. That works fine for simple forwarding, but it breaks the moment you need per-item routing, partial failure handling, or granular observability. The Zerq workflow builder has a loop_node that changes this.
Why forwarding the whole batch is not enough
When a gateway proxies an array of events to a single backend endpoint, the backend absorbs all the orchestration responsibility. Item 17 fails? The backend either fails the entire batch and forces the sender to resend all 200 items, or it builds internal partial-success tracking that pollutes its domain logic. Either way, the complexity lives where it does not belong.
Platforms like Kong, Apigee, and AWS API Gateway are gateway proxies at heart. You can build plugins in Lua (Kong) or policies in XML (Apigee), and for simple header manipulation or rate limiting that is fine. But iterating over a JSON array inside a request body, making a separate upstream call per item, and aggregating the results back into a single response is not something a proxy policy is designed to do. You end up with a Lambda function, a sidecar service, or a custom plugin that defeats the purpose of a managed gateway layer.
MuleSoft DataWeave can do array iteration, but it ships as a cloud-hosted integration platform and requires either the MuleSoft Anypoint runtime or CloudHub. For regulated industries that need everything on-premises, that constraint is often a blocker. Zerq's workflow builder runs inside the gateway binary itself — on your infrastructure, with no external runtime dependency, and with every execution captured in the same audit trail as every other API request.
How Zerq handles API gateway batch processing
The Zerq workflow builder attaches a visual node graph to any proxy. When a request matches the proxy's collection and endpoint, the gateway hands the request to the workflow rather than forwarding it directly. The workflow can then read the request body, iterate over the batch using a loop_node, make per-item calls, and return a composed response.
The workflow topology
A batch processing workflow has this shape:
http_trigger
│
▼
validate_node ─── (invalid) ──▶ response_node [400]
│ (valid)
▼
loop_node ─── Loop ──▶ http_request_node ─── (success) ──▶ set_node [build_success]
│ └── (error) ──▶ set_node [build_error]
│ Done
▼
response_node [200 — aggregated results]
The loop_node has two output handles: Loop (runs once per item in the array) and Done (runs once after all iterations complete, receiving the aggregated results array). Every node connected from the Loop handle executes once per item. Every node connected from the Done handle runs once at the end and has access to $json['loop_events'].results, an array of per-item outputs.
Step-by-step setup
-
Open the Zerq management UI. Navigate to Collections, open the collection that owns the batch endpoint, and click into the proxy.
-
Click Edit Workflow on the proxy detail page. The workflow canvas opens with an
http_triggernode already placed. -
Click Add node in the toolbar to open the node palette. Add a
validate_node. Set its Value input to{{ $json['http_trigger'].request.body }}. In the Schema field, paste the JSON Schema for your batch payload:
{
"type": "object",
"required": ["events"],
"properties": {
"events": {
"type": "array",
"minItems": 1,
"maxItems": 200,
"items": {
"type": "object",
"required": ["id", "type", "amount"],
"properties": {
"id": { "type": "string" },
"type": { "type": "string", "enum": ["payment.completed", "payment.failed", "payment.refunded"] },
"amount": { "type": "integer", "minimum": 0 }
}
}
}
}
}
-
Wire the
validate_node's valid output to a newloop_node. Wire the invalid output to aresponse_nodeconfigured to return status400with a body of{{ $json['validate_batch'].error }}. -
Configure the
loop_node. Set Array expression to:
{{ $json['http_trigger'].request.body.events }}
Set Max iterations to 200 (matching the schema constraint). Name the node loop_events.
- Add an
http_request_node. Wire it from the Loop output ofloop_events. Configure it:
{
"config": {
"timeout_ms": 5000,
"retry_config": { "max_attempts": 2, "backoff_ms": 500 }
},
"inputs": {
"url": "https://payments.internal/v1/process",
"method": "POST",
"headers": {
"Content-Type": ["application/json"],
"X-Idempotency-Key": ["{{ $json['$item'].id }}"]
},
"body": {
"type": "json",
"content": {
"eventId": "{{ $json['$item'].id }}",
"eventType": "{{ $json['$item'].type }}",
"amount": "{{ $json['$item'].amount }}"
}
}
}
}
The $json['$item'] expression references the current array element for this iteration. $json['$index'] gives the zero-based position. The X-Idempotency-Key header uses the event ID, so the backend can safely deduplicate retried calls.
- Add two
set_nodeinstances — one on thesuccessbranch ofcall_handler, one on theerrorbranch.
On the success branch:
{
"config": {
"assignments": {
"id": "{{ $json['$item'].id }}",
"status": "processed",
"index": "{{ $json['$index'] }}"
}
}
}
On the error branch:
{
"config": {
"assignments": {
"id": "{{ $json['$item'].id }}",
"status": "failed",
"index": "{{ $json['$index'] }}"
}
}
}
Both set_node instances are the terminal nodes for their respective branches. The loop_node collects the output of whichever one runs per iteration into results.
- Add a final
response_nodewired from the Done output ofloop_events. Set its body to:
{{ JSON.stringify({
total: $json['loop_events'].count,
results: $json['loop_events'].results
}) }}
Set status to 200. This returns a payload like:
{
"total": 3,
"results": [
{ "id": "evt_001", "status": "processed", "index": 0 },
{ "id": "evt_002", "status": "failed", "index": 1 },
{ "id": "evt_003", "status": "processed", "index": 2 }
]
}
-
Click Validate in the workflow builder toolbar to check the workflow for missing required fields, invalid expressions, and disconnected nodes. The builder highlights the specific node and field with a problem.
-
Select the
http_triggernode and click Listen for Request in the config panel. Copy the generated test URL and POST a sample batch payload to it. The builder captures the payload, runs the workflow, and shows the output of each node — including the per-item loop results — in a single pass. -
Click Save to persist the workflow definition. Then click the Enable Workflow toggle in the toolbar to activate production routing. From this point, all traffic to the proxy's endpoint passes through the workflow engine.
What happens when an item fails
When the http_request_node hits its error branch — either because the backend returned a non-2xx status or the timeout was exceeded — the set_node [build_error] runs. That node's output becomes the entry in loop_events.results for that iteration. The loop continues to the next item. A failure in item 17 does not abort the loop or affect items 18 through 200.
This matters operationally. The sender receives a structured response that identifies exactly which events succeeded and which did not. They can resend only the failed items. Your backend receives idempotent per-event calls that it can safely handle in isolation, rather than a blob it has to partially re-process.
Routing to different backends by event type
If payment.completed and payment.refunded need to reach different services, replace the http_request_node with a condition_node upstream of it:
{
"config": {
"conditions": [
{
"output": "completed",
"expression": "{{ $json['$item'].type === 'payment.completed' }}"
},
{
"output": "refunded",
"expression": "{{ $json['$item'].type === 'payment.refunded' }}"
}
],
"default_output": "other"
}
}
Wire the completed and refunded branches to separate http_request_node instances pointing at different upstream URLs. Each branch ends at its own set_node. The loop still aggregates all results from all branches into a single results array after the Done edge fires.
What this looks like in request logs
Every batch POST to the workflow endpoint generates one request log entry in Zerq's observability layer. That entry captures:
- Request ID — the trace ID returned to the caller in
X-Request-ID, useful for correlating a batch call across systems - Client ID and Profile ID — which client sent the batch and under which credential set, so a compliance team can immediately identify the sender
- Request body — the full batch payload (all 50 or 200 events)
- Response body — the aggregated results array showing per-item status
- Latency — the total time from request receipt to response, covering all loop iterations
- Status code — the final HTTP status returned to the caller
A compliance or operations team reviewing logs can open the request detail, see the complete batch payload, and read the exact per-item outcome from the response body — without needing to correlate across multiple log entries. Filtering by Client ID and a time range gives a full picture of what a specific partner sent and how each batch resolved.
What this looks like in practice
A fintech platform processes bulk payment events from a card network partner. The partner sends a nightly reconciliation batch — up to 150 events per webhook call — to the platform's /v1/payments/reconcile endpoint.
Before Zerq: the platform's reconciliation service received the full array, iterated internally, and wrote its own retry logic for failed items. When the card network changed its event schema, the reconciliation service needed a code change and a deploy. The service also had to call three downstream systems in sequence per event (ledger update, notification service, fraud flag), making it slow and fragile.
After Zerq: the workflow at /v1/payments/reconcile validates the incoming batch against the schema, iterates with a loop_node, routes each event type to the correct backend via a condition_node, and returns the aggregated result. When the card network added a new payment.disputed event type, the platform added a new branch to the condition_node in the Zerq UI — no service deploy, no code change. The workflow validation immediately rejected batches that did not include the new required fields, protecting the backend from malformed data during the rollout period.
The access control layer ensures the card network partner can only reach the reconcile endpoint, not the admin or reporting routes. The profile assigned to their client uses mTLS for mutual authentication, with an IP allowlist scoped to the card network's known egress range. Every batch call shows up in the request logs tied to that client ID, making it straightforward to answer "how many events did we receive from Visa between Monday and Wednesday?" without querying the reconciliation database.
What you get from the gateway owning the loop
Moving the batch iteration into the gateway layer does three things that backend code cannot easily replicate:
The loop runs in the gateway path under the same authentication, rate limiting, and access profile as any other request. There is no separate service to deploy, credential to manage, or audit trail to reconcile. Every execution flows through the same observability layer that handles all other traffic.
Schema validation runs before the loop starts. A malformed batch — missing required fields, wrong event types, array length violations — fails immediately with a structured error response before any backend service is called. The backend only ever receives well-formed, validated items.
Changing the processing logic is a UI operation. Add a new event type branch, update the schema, change the timeout or retry behavior — all in the workflow builder without a service deploy. Changes are versioned and audited as config events in the Zerq audit log, so compliance teams can see exactly when the workflow changed and who made the change.
Zerq is an enterprise API gateway built for regulated industries — one platform for API management, AI agent access, compliance audit, and developer portal, running entirely in your own infrastructure. See how it works or request a demo to walk through your specific requirements.