# Middleware Nodes

Middleware Nodes call your external system while a visitor is moving through a funnel. FunnelFlux waits for the response, evaluates the middleware routes, then follows the matched or default funnel connection.

Use Middleware Nodes when an external system must make a live routing decision. If you only need to notify another system and let the funnel continue, use a Webhook Node instead.

## Middleware vs Webhooks

Middleware Nodes and Webhook Nodes both send funnel context to an external URL, but they have different runtime contracts:

| Feature | Middleware Node | Webhook Node |
| --- | --- | --- |
| Runtime behavior | Synchronous decision point | Asynchronous pass-through dispatch |
| Funnel waits for response | Yes | No |
| Response affects routing | Yes | No |
| Main event type | `middleware_request` | `node_execution`, `on_action`, `conversion_triggered`, or `custom_event_triggered` |
| Typical use | Lead validation, fraud check, CRM decision, host-and-post | Notifications, logging, background automation |

Because middleware runs in the live routing path, endpoint latency and reliability directly affect the visitor. Always configure a safe default route.

## API Workflow

Middleware configs are reusable assets. A funnel Middleware Node references one middleware config, then connects each navigable route to later funnel nodes.

The usual flow is:

1. Create or update a middleware config through the Assets API.
2. Add a Middleware Node to the funnel and reference that config.
3. Connect every middleware route through `connectionMiddlewareParams.onRouteId`.
4. Connect the `default` route as the fallback route.
5. Send test traffic and check middleware logs/reporting.

See the [Middleware Configs API](/api/assets/middlewareconfig/list) for endpoint and schema details.

Use the [Middleware Function Templates](/middleware-templates) when building a receiver endpoint.

## Request Delivery

Middleware destination settings control the URL, method, timeout, headers, auth, and signing.

POST requests send a JSON body. GET requests send no body.

For GET requests with HMAC signing enabled, FunnelFlux signs the resolved URL. For POST requests with HMAC signing enabled, FunnelFlux signs the JSON body.

Middleware requests include these FunnelFlux headers:

| Header | Description |
| --- | --- |
| `X-FunnelFlux-Schema-Version` | Same value as body `schema_version`, currently `1.0`. |
| `X-FunnelFlux-Request-ID` | Request correlation ID. Same value as body `request_id` for POST. |
| `X-FunnelFlux-Signature` | Optional base64 HMAC-SHA256 signature when the config has an HMAC secret. |

Configured custom headers are also sent. If basic auth is configured, FunnelFlux sets the standard `Authorization` header.

FunnelFlux token placeholders are plaintext-only for middleware destination URLs. They are not expanded inside custom header values.

## Destination Secrets

Middleware config read APIs sanitize stored secrets:

- `headers` are returned with their stored values.
- `basicAuth.username` is returned.
- `basicAuth.password` is never returned as the real stored value.
- `hmacSecret` is never returned as the real stored value.
- `hasBasicAuthPassword` is true when a non-blank basic-auth password is stored. In that case, `basicAuth.password` is returned as the fixed placeholder `********`.
- `hasHmacSecret` is true when a non-blank HMAC secret is stored. In that case, `hmacSecret` is returned as the fixed placeholder `********`.

When saving an existing middleware config, clients can preserve hidden secret values by sending the corresponding `hasBasicAuthPassword` or `hasHmacSecret` flag as `true` while omitting the secret field, sending it blank, or sending the unchanged `********` placeholder.

To replace a stored secret, send a real non-blank `basicAuth.password` or `hmacSecret`.

To clear the stored HMAC secret, send `hasHmacSecret: false` with `hmacSecret` omitted, null, or blank. To clear only the basic-auth password while keeping the username, send `hasBasicAuthPassword: false` with the password omitted or blank. To clear basic auth entirely, remove `basicAuth` from the config.

## Node Destination Overrides

Funnel middleware nodes can override selected destination settings from the referenced middleware config through `nodeMiddlewareRefParams.destinationOverrides`.

Supported node-level overrides are:

- `headers`
- `basicAuth.username`
- `basicAuth.password`
- `hmacSecret`
- `hasBasicAuthPassword`
- `hasHmacSecret`

Node-level headers replace middleware config headers with the same name and add any new header names. Node overrides do not change the destination URL, method, or destination type.

REST read responses mask node-level `basicAuth.password` and `hmacSecret` the same way they mask middleware config secrets: set secrets return `********` plus the corresponding `hasBasicAuthPassword` or `hasHmacSecret` flag. On save, send the unchanged `********` placeholder, a blank value, or omit the secret field while keeping the matching flag true to preserve the stored node-level secret. Send a real non-blank value to replace it. Send the matching flag false with the secret omitted or blank to clear it.

## POST Payload

POST middleware requests use JSON field names in `snake_case`.

| Field | Type | Description |
| --- | --- | --- |
| `schema_version` | string | Current body version, currently `1.0`. |
| `event_type` | string | Always `middleware_request`. |
| `request_id` | string | Correlates the outbound request, headers, route decision, and logs. |
| `timestamp` | integer | UTC Unix timestamp in milliseconds. |
| `execute_timeout_ms` | integer | Effective Edge execution budget for this request, after defaults and clamping. Return before this deadline and keep your own safety buffer. |
| `is_action` | boolean | Whether the middleware run follows an action request. |
| `action_number` | integer | Present when `is_action` is true. |
| `visitor` | object | Visitor, geo, device, browser, and connection context when available. |
| `funnel` | object | Current funnel and middleware node identifiers/names. |
| `traffic_source` | object | Traffic source ID and name when available. |
| `hit` | object | Unified hit context when a hit exists. Treat this object and all nested fields as optional. |
| `session` | object | Live Edge session identifiers and current-hit timestamps when available. |
| `tracking_fields` | object | Entrance tracking fields captured from the traffic source URL. |
| `url_buffer` | object | Current accumulated URL/action params. |
| `action_data` | object | Non-system params from the current action plus origin fields. |
| `visitor_tags` | array | Visitor tag IDs snapshot when available. |

Use `url_buffer` for accumulated URL and action parameters.

Middleware POST requests can include both a top-level `hit` object and `session` identifiers. Treat both as optional. Do not fail the request because either object is empty or missing.

### Visitor Fields

Common `visitor` keys:

```text
visitor_id
ip
user_agent
country_code
country_name
region
city
timezone
language
device_type
os
browser
connection_type
```

### Session Fields

Common `session` keys:

```text
visitor_id
hit_id
entrance_id
entrance_timestamp
hit_timestamp
reference_hit_id
most_recent_page_id
referrer
```

`visitor.visitor_id` identifies the visitor. `session.visitor_id` comes from the live Edge session and can repeat that visitor value. Use `session.hit_id`, `session.entrance_id`, and `session.reference_hit_id` for current hit and entrance routing context.

### Action Data

`action_data` contains non-system action query/body values plus action origin metadata. It does not duplicate top-level `is_action` or `action_number`.

Common origin metadata keys:

| Field | Description |
| --- | --- |
| `origin_node_id` | Node ID that originated the action. |
| `origin_node_name` | Origin node name. |
| `origin_node_type` | Origin node type. |
| `origin_page_id` | Resolved origin page ID when available. |
| `origin_page_name` | Resolved origin page name when available. |

## Example POST Payload

```json
{
  "schema_version": "1.0",
  "event_type": "middleware_request",
  "request_id": "req-123",
  "execute_timeout_ms": 5000,
  "is_action": true,
  "action_number": 2,
  "timestamp": 1710072000123,
  "visitor": {
    "visitor_id": "vis-123",
    "ip": "203.0.113.42",
    "user_agent": "Mozilla/5.0",
    "country_code": "US",
    "country_name": "United States",
    "region": "CA",
    "city": "Los Angeles",
    "timezone": "America/Los_Angeles",
    "language": "en-US",
    "device_type": "desktop",
    "os": "Windows",
    "browser": "Chrome",
    "connection_type": "wifi"
  },
  "funnel": {
    "funnel_id": "fun-123",
    "funnel_name": "Lead Capture Funnel",
    "node_id": "node-mw-1",
    "node_name": "Email Validation"
  },
  "traffic_source": {
    "traffic_source_id": "ts-123",
    "name": "Facebook"
  },
  "hit": {},
  "session": {
    "visitor_id": "vis-123",
    "hit_id": "hit-123",
    "entrance_id": "ent-123",
    "entrance_timestamp": 1710071000000,
    "hit_timestamp": 1710072000123,
    "reference_hit_id": "ref-hit-123",
    "most_recent_page_id": "page-123",
    "referrer": "https://example.com/from"
  },
  "tracking_fields": {
    "utm_source": "facebook"
  },
  "url_buffer": {
    "email": "user@example.com"
  },
  "action_data": {
    "email": "user@example.com",
    "origin_node_id": "node-origin",
    "origin_node_name": "Landing Form",
    "origin_node_type": "page",
    "origin_page_id": "page-origin",
    "origin_page_name": "Landing Page"
  },
  "visitor_tags": ["high_value"]
}
```

## Response Evaluation

FunnelFlux captures the response status and up to 64 KiB of response body. It then evaluates non-default routes in ascending `priority` order.

Exactly one default route is required. If no non-default route matches, or if runtime delivery fails, FunnelFlux uses the default route.

Routes can evaluate these fields:

| Field | Description |
| --- | --- |
| `status_code` | HTTP status code from the middleware response. Values must be integer HTTP codes from `100` to `599`. |
| `body_text` | Response body as text. Supports text operators. |
| `body_json.<path>` | JSON response value by dot path, such as `body_json.decision` or `body_json.items.9.score`. |

Supported operators:

```text
is
is_not
contains
not_contains
is_empty
is_not_empty
greater_than
greater_than_or_equal
less_than
less_than_or_equal
```

`body_json.<path>` paths use dot segments only. Brackets, quoted JSONPath syntax, spaces, empty segments, trailing dots, leading dots, and double-dot segments are rejected.

`status_code` condition values must be strict integer HTTP codes from `100` through `599`; signs, decimals, padding, and spaces are rejected. `contains` and `not_contains` are case-insensitive. Numeric comparisons require finite numeric values and apply to `status_code` and `body_json.<path>`.

## Route Keys

Middleware routes use stable route keys:

| Field | Description |
| --- | --- |
| `idRoute` | Lowercase route key and user-facing route label. Use `a-z`, `0-9`, and `_`, up to 64 characters. |
| `priority` | Evaluation order. The default route uses `0`; non-default routes use `1` through `255`. |
| `isDefault` | Marks the fallback route. Exactly one route must be default. |

The reserved fallback route is `idRoute: "default"` with `priority: 0`. Non-default routes are evaluated by ascending `priority`.

Funnel connections leaving a Middleware Node use `connectionMiddlewareParams.onRouteId`. Set it to the matching route key, such as `approved`, `review`, or `default`.

## Example Response

Your middleware endpoint might return:

```json
{
  "decision": "approved",
  "score": 95,
  "request_id": "req-123"
}
```

A middleware config could then route:

| Route | Condition | Result |
| --- | --- | --- |
| `approved` | `body_json.decision` `is` `approved` | Continue through the approved funnel connection. |
| `review` | `body_json.score` `less_than` `50` | Continue through a review/reject path. |
| `default` | fallback | Continue through the safe fallback connection. |

Middleware routes always continue through funnel connections. A broken route binding or missing target node is a funnel configuration error.

## Timeouts and Fallback

`timeoutMs` accepts `1` through `30000`; runtime clamps to 30 seconds and defaults to 5 seconds.

POST payloads include `execute_timeout_ms`, the effective timeout budget Edge is enforcing for that specific request. Your endpoint should finish external API calls below `execute_timeout_ms - 250` and return a fallback JSON response before the Edge deadline.

The default route is used for request-local failures, including:

- missing, inactive, or unavailable middleware config
- billing denial
- invalid destination
- unsupported method
- timeout
- request failure
- no matching non-default route
- missing `default` route binding from the funnel node

There is no sticky circuit breaker or pod-wide disablement. A failure falls back for that request only.

## Destination Restrictions

In production, middleware destinations must be public HTTPS endpoints. HTTP is accepted only in development environments. FunnelFlux revalidates the resolved host at execution time and blocks loopback, private, link-local, metadata, and reserved IP ranges.

Use a hosted endpoint or a public tunnel for local testing. Do not rely on localhost, private VPC IPs, or metadata service addresses.

## Common Mistakes

- Using Middleware Nodes for simple notifications. Use Webhook Nodes when the response should not affect routing.
- Forgetting to wire the `default` route.
- Building slow middleware endpoints. This is live redirect-path work.
- Changing middleware route keys without checking existing funnel connections.
- Hardcoding local/private URLs for production traffic.
