← Back to ArticlesAPI Management

API Management — Solving Tracking Issues with send-request Policy: mode=\

Deep dive into the send-request policy — why tracking breaks, how mode=new fixes it, when to use mode=copy, and every real-world pattern you need.

API Management — Solving Tracking Issues with send-request: mode="new" vs mode="copy"

The Problem

You have an APIM policy that calls an external service using send-request — maybe to log an audit event, enrich a request with data from another API, or notify a webhook. Everything looks fine in development. Then in production you start seeing:

All of these are tracking and context-leakage issues — and they all trace back to one thing: not understanding the difference between mode="new" and mode="copy" in the send-request policy.


What Is the send-request Policy?

send-request lets an APIM policy make an outbound HTTP call to any URL — during request processing, before the backend is called, or during the response. It is the Swiss Army knife of APIM policies.

<send-request mode="new" response-variable-name="myResponse" timeout="10" ignore-error="false">
    <set-url>https://my-internal-service.com/api/notify</set-url>
    <set-method>POST</set-method>
    <set-header name="Content-Type" exists-action="override">
        <value>application/json</value>
    </set-header>
    <set-body>{"event": "api-called"}</set-body>
</send-request>

What You Can Do With It

Client Request
      │
      ▼
┌─────────────────────────────────────────────────────────────┐
│  APIM Inbound Policy                                        │
│                                                             │
│  ① Enrich  ──► send-request to data service                │
│              ◄── get user profile, inject into request      │
│                                                             │
│  ② Auth    ──► send-request to token validation service    │
│              ◄── validate token, allow or deny              │
│                                                             │
│  ③ Audit   ──► send-request to logging/audit service       │
│              (fire-and-forget, ignore-error="true")         │
└─────────────────────────────────────────────────────────────┘
      │
      ▼
  Backend API
      │
      ▼
┌─────────────────────────────────────────────────────────────┐
│  APIM Outbound Policy                                       │
│                                                             │
│  ④ Enrich  ──► send-request to transform response data     │
│  ⑤ Notify  ──► send-request to webhook on success          │
└─────────────────────────────────────────────────────────────┘

The response-variable-name

The result of send-request is stored in a context variable you name. You can then read it:

<send-request mode="new" response-variable-name="tokenValidationResult" timeout="5">
    <set-url>https://auth.internal/validate</set-url>
    <set-method>POST</set-method>
    <set-body>@(context.Request.Headers.GetValueOrDefault("Authorization", ""))</set-body>
</send-request>

<!-- Read the result later in the same policy -->
<set-variable name="isValid" value="@{
    var result = (IResponse)context.Variables["tokenValidationResult"];
    return result.StatusCode == 200;
}" />

The Core Difference: mode="new" vs mode="copy"

This is the most important concept in send-request. Every tracking and context-leakage bug you will ever hit comes from using the wrong mode.

mode="copy" — Inherits Everything From the Original Request

When you use mode="copy", APIM starts the outbound request as a full copy of the current inbound request. This means:

mode="copy" behaviour:
┌──────────────────────────────────────────────────────┐
│  Original Client Request                             │
│  Headers:                                            │
│    Authorization: Bearer eyJhbGc...                  │
│    Request-Id: abc-123                               │
│    X-Custom-Client-Header: sensitive-value           │
│    Cookie: session=xyz                               │
│  Body: { "userId": 42 }                              │
└──────────────────┬───────────────────────────────────┘
                   │  mode="copy" clones ALL of this
                   ▼
┌──────────────────────────────────────────────────────┐
│  send-request outbound call                          │
│  Headers:  ← ALL copied, even ones you didn't want   │
│    Authorization: Bearer eyJhbGc...  ← LEAKED        │
│    Request-Id: abc-123               ← DUPLICATED    │
│    X-Custom-Client-Header: sensitive-value ← LEAKED  │
│    Cookie: session=xyz               ← LEAKED        │
│    Content-Type: application/json    ← your addition │
│  Body: { "userId": 42 }  ← ORIGINAL body, not yours  │
└──────────────────────────────────────────────────────┘

mode="new" — Starts With a Blank Slate

When you use mode="new", APIM creates a completely fresh, empty request. Nothing from the original request is inherited. You explicitly set every header, body, method, and URL you want.

mode="new" behaviour:
┌──────────────────────────────────────────────────────┐
│  Original Client Request                             │
│  Headers: Authorization, Request-Id, Cookie...       │
│  Body: { "userId": 42 }                              │
└──────────────────┬───────────────────────────────────┘
                   │  mode="new" → completely ignored
                   ▼
┌──────────────────────────────────────────────────────┐
│  send-request outbound call                          │
│  Headers:  ← ONLY what you explicitly set            │
│    Content-Type: application/json    ← yours         │
│    Request-Id: your-generated-guid   ← yours         │
│    X-Api-Key: {{InternalApiKey}}     ← yours         │
│  Body: { "event": "api-called" }     ← yours         │
│  Method: POST                        ← yours         │
└──────────────────────────────────────────────────────┘

Why Tracking Breaks With mode="copy"

Here are the exact scenarios where mode="copy" causes tracking and context issues.

Problem 1: Correlation ID Duplication

Your observability setup injects a Request-Id into every request. With mode="copy", the external call already has Request-Id copied from the client. If your fragment or policy then tries to set it again:

<!-- mode="copy" — Request-Id already exists from the client request -->
<send-request mode="copy" response-variable-name="auditResult" timeout="5">
    <set-url>https://audit.internal/log</set-url>
    <set-method>POST</set-method>
    <!-- You try to set it, but exists-action="skip" means the COPIED one wins -->
    <set-header name="Request-Id" exists-action="skip">
        <value>@(context.Variables.GetValueOrDefault<string>("requestId"))</value>
    </set-header>
</send-request>

Result: The audit service receives the client's original correlation ID, not the one your gateway generated. Your trace is now broken — the audit log and the gateway log have different IDs for the same transaction.

Even worse with exists-action="override": You override it, but now the copied body is still the original client body — not your audit payload.

Problem 2: Authorization Token Leaking to Internal Services

<!-- Dangerous: mode="copy" sends the client's Bearer token to your internal audit service -->
<send-request mode="copy" response-variable-name="auditLog" timeout="3" ignore-error="true">
    <set-url>https://audit.internal/events</set-url>
    <set-method>POST</set-method>
    <set-body>{"event": "order-placed", "orderId": "123"}</set-body>
    <!-- You set the body correctly, but the Authorization header was already copied -->
    <!-- Your audit service now receives the client's Bearer token it didn't ask for -->
</send-request>

Result: Your internal audit service sees an Authorization: Bearer <client-token> header. If your audit service validates tokens, it may reject the call because the client token wasn't issued for the audit service audience. Security teams will flag this as credential forwarding.

Problem 3: Unexpected Headers Break Downstream Services

Client sends non-standard headers: X-Requested-With, X-Debug-Mode: true, Accept-Language, Referer. With mode="copy" all of these land on your internal service. Some internal services have strict header validation and reject unknown headers with 400 Bad Request. You spend hours debugging why your send-request is intermittently failing — the answer is a random header the client sometimes sends.

Problem 4: Body Mismatch

With mode="copy", the body starts as the original client request body. If you then set a new body with <set-body>, it overwrites — but only if the Content-Type is compatible. In some edge cases (multipart requests, binary bodies), the copied body can interfere with your intended payload.

Problem 5: Third-Party Caller Sends Their Own Request-Id — You Lose It or Misuse It

This is a real-world scenario that catches teams off guard. A third-party partner calls your main API and includes their own request tracking header — typically Request-Id, X-Request-Id, or traceparent. This is their ID, generated on their side, used to correlate the call in their own logs.

Third-Party System                  Your APIM Gateway
      │                                    │
      │  POST /api/orders                  │
      │  Request-Id: tp-abc-789  ─────────►│
      │  Authorization: Bearer <token>     │
      │  Content-Type: application/json    │
      │                                    │

There are two problems that commonly occur here:

Problem A — You discard their Request-Id entirely:

Your gateway generates a new Request-Id and never records the third party's Request-Id. When the third party opens a support ticket saying "our request tp-abc-789 failed", you have no way to find it in your logs — their ID and your ID were never linked.

<!-- Bad: generates a new ID, third-party's Request-Id is silently thrown away -->
<set-variable name="requestId" value="@(Guid.NewGuid().ToString())" />

Problem B — With mode="copy", their Request-Id leaks into your internal send-request calls:

Your audit service, enrichment service, or token service now receives Request-Id: tp-abc-789 in its headers — a third-party ID it has no context for. Worse, if your internal services use Request-Id for their own tracing, the third-party value corrupts their trace chain.

<!-- Dangerous: mode="copy" forwards the third-party's Request-Id 
     to your internal audit service as-is -->
<send-request mode="copy" response-variable-name="auditResult" timeout="3" ignore-error="true">
    <set-url>https://audit.internal/events</set-url>
    <set-method>POST</set-method>
    <!-- audit.internal now receives Request-Id: tp-abc-789 from the third party -->
    <!-- Your internal trace is now polluted with an external ID -->
</send-request>

The correct approach — capture their Request-Id, link it to yours, keep them separate:

<!-- Step 1: Read the third-party's Request-Id from the incoming request -->
<set-variable name="thirdPartyRequestId" value="@{
    var tpId = context.Request.Headers.GetValueOrDefault("Request-Id", "");
    if (string.IsNullOrEmpty(tpId)) {
        tpId = context.Request.Headers.GetValueOrDefault("X-Request-Id", "");
    }
    return tpId;
}" />

<!-- Step 2: Generate your own internal correlation ID regardless -->
<set-variable name="requestId" value="@(Guid.NewGuid().ToString())" />

<!-- Step 3: Expose your correlation ID in the response 
     so the third party can also reference it in support tickets -->
<set-header name="Request-Id" exists-action="override">
    <value>@(context.Variables.GetValueOrDefault<string>("requestId"))</value>
</set-header>

<!-- Step 4: In your audit send-request (mode="new"), log BOTH IDs together -->
<send-request mode="new" response-variable-name="auditIgnored" timeout="2" ignore-error="true">
    <set-url>https://audit.internal/events</set-url>
    <set-method>POST</set-method>
    <set-header name="Content-Type" exists-action="override">
        <value>application/json</value>
    </set-header>
    <set-header name="X-Api-Key" exists-action="override">
        <value>{{AuditApiKey}}</value>
    </set-header>
    <!-- Your correlation ID on the wire — not the third party's -->
    <set-header name="Request-Id" exists-action="override">
        <value>@(context.Variables.GetValueOrDefault<string>("requestId"))</value>
    </set-header>
    <set-body>@{
        return new JObject(
            new JProperty("requestId", context.Variables.GetValueOrDefault<string>("requestId")),
            // Link third-party ID in the payload — now both IDs are searchable in your logs
            new JProperty("thirdPartyRequestId", context.Variables.GetValueOrDefault<string>("thirdPartyRequestId", "")),
            new JProperty("api", context.Api.Name),
            new JProperty("operation", context.Operation.Name),
            new JProperty("subscriptionId", context.Subscription?.Id ?? "anonymous"),
            new JProperty("timestamp", DateTime.UtcNow.ToString("o"))
        ).ToString();
    }</set-body>
</send-request>

Result: Your audit log now contains both requestId: "your-guid" and thirdPartyRequestId: "tp-abc-789". When a third-party partner raises a support ticket with their ID, you can search either field and find the exact transaction. Their ID never leaks into your internal service headers — it is recorded as data in the audit payload only.

Audit Log Entry:
{
  "requestId":      "f3a1b2c4-...",   ← your internal ID, on all headers
  "thirdPartyRequestId": "tp-abc-789",    ← their ID, in payload only, searchable
  "api":                "Orders API",
  "operation":          "CreateOrder",
  "subscriptionId":     "sub-partner-xyz",
  "timestamp":          "2026-01-12T10:30:00Z"
}

The Fix: Always Use mode="new" for Internal Calls

The rule is simple:

If you are making a call to an internal service, a webhook, a logging endpoint, or any service that is NOT supposed to receive the client's context — use mode="new".

<!-- CORRECT: mode="new" — full control, zero leakage -->
<send-request mode="new" response-variable-name="auditResult" timeout="5" ignore-error="true">
    <set-url>https://audit.internal/events</set-url>
    <set-method>POST</set-method>
    <set-header name="Content-Type" exists-action="override">
        <value>application/json</value>
    </set-header>
    <set-header name="X-Api-Key" exists-action="override">
        <value>{{AuditServiceApiKey}}</value>
    </set-header>
    <set-header name="Request-Id" exists-action="override">
        <value>@(context.Variables.GetValueOrDefault<string>("requestId"))</value>
    </set-header>
    <set-body>@{
        return new JObject(
            new JProperty("event", "api-called"),
            new JProperty("api", context.Api.Name),
            new JProperty("operation", context.Operation.Name),
            new JProperty("subscriptionId", context.Subscription.Id),
            new JProperty("timestamp", DateTime.UtcNow.ToString("o")),
            new JProperty("requestId", context.Variables.GetValueOrDefault<string>("requestId"))
        ).ToString();
    }</set-body>
</send-request>

When IS mode="copy" the Right Choice?

mode="copy" is useful in a narrow, specific scenario: when you are acting as a transparent proxy and want to forward the original request — headers and all — to a different backend, with minimal modifications.

Legitimate Use Case: Request Forwarding / Shadow Traffic

<!-- Shadow traffic: send a copy of the real request to a new service version for testing -->
<!-- You WANT all the original headers forwarded — that's the whole point -->
<send-request mode="copy" response-variable-name="shadowResponse" timeout="3" ignore-error="true">
    <set-url>https://new-backend-v2.internal/api/orders</set-url>
    <!-- No header overrides — you want exactly what the client sent -->
    <!-- No body override — you want exactly what the client sent -->
</send-request>
<!-- Discard shadowResponse — only used for comparison testing, real response still comes from v1 -->

Legitimate Use Case: Protocol Bridge With Full Context Pass-Through

<!-- Forwarding to a secondary backend that needs the full client context:
     same auth token, same correlation chain, same body -->
<send-request mode="copy" response-variable-name="secondaryResult" timeout="10">
    <set-url>https://secondary-backend.internal/process</set-url>
    <!-- Only override what's different — everything else passes through intentionally -->
    <set-header name="X-Forwarded-Host" exists-action="override">
        <value>@(context.Request.OriginalUrl.Host)</value>
    </set-header>
</send-request>

Decision Table

ScenarioCorrect ModeReason
Audit / logging callnewInternal service, no client context needed
Webhook notificationnewOnly your payload, no client headers
Token validation callnewYou control exactly what you send
Data enrichment callnewClean request, only your params
Third-party calls with their own Request-IdnewCapture their ID as audit payload data; keep wire headers clean
Shadow traffic testingcopyYou want identical replica of client request
Transparent proxy forwardingcopyFull client context pass-through is intentional
A/B routing to new backendcopySame request, different destination

Benefits of send-request Policy

Beyond fixing the mode confusion, it helps to understand why send-request is so powerful and when to reach for it.

1. Pre-Request Enrichment

Fetch data before the backend sees the request. Inject it as a header or body field:

<!-- Fetch user profile and inject tier into the request -->
<send-request mode="new" response-variable-name="userProfile" timeout="5">
    <set-url>@("https://users.internal/profile/" + context.Request.Headers.GetValueOrDefault("X-User-Id", ""))</set-url>
    <set-method>GET</set-method>
    <set-header name="X-Api-Key" exists-action="override">
        <value>{{UserServiceApiKey}}</value>
    </set-header>
</send-request>

<set-variable name="userTier" value="@{
    var profile = (IResponse)context.Variables["userProfile"];
    var body = profile.Body.As<JObject>();
    return body["tier"]?.ToString() ?? "free";
}" />

<set-header name="X-User-Tier" exists-action="override">
    <value>@(context.Variables.GetValueOrDefault<string>("userTier"))</value>
</set-header>

2. External Token Acquisition (On-Behalf-Of)

Fetch a service-to-service token before calling a protected backend:

<!-- Get a token for the backend service -->
<send-request mode="new" response-variable-name="backendToken" timeout="10">
    <set-url>https://login.microsoftonline.com/{{TenantId}}/oauth2/v2.0/token</set-url>
    <set-method>POST</set-method>
    <set-header name="Content-Type" exists-action="override">
        <value>application/x-www-form-urlencoded</value>
    </set-header>
    <set-body>@{
        return "grant_type=client_credentials"
            + "&client_id={{ClientId}}"
            + "&client_secret={{ClientSecret}}"
            + "&scope={{BackendScope}}";
    }</set-body>
</send-request>

<set-variable name="accessToken" value="@{
    var tokenResponse = (IResponse)context.Variables["backendToken"];
    var body = tokenResponse.Body.As<JObject>();
    return body["access_token"]?.ToString() ?? "";
}" />

<set-header name="Authorization" exists-action="override">
    <value>@("Bearer " + context.Variables.GetValueOrDefault<string>("accessToken"))</value>
</set-header>

3. Fire-and-Forget Audit Logging

Use ignore-error="true" so a logging failure never blocks the real request:

<!-- Non-blocking audit log — client never waits for this -->
<send-request mode="new" response-variable-name="auditIgnored" timeout="2" ignore-error="true">
    <set-url>https://audit.internal/events</set-url>
    <set-method>POST</set-method>
    <set-header name="Content-Type" exists-action="override">
        <value>application/json</value>
    </set-header>
    <set-header name="X-Api-Key" exists-action="override">
        <value>{{AuditApiKey}}</value>
    </set-header>
    <set-body>@{
        return new JObject(
            new JProperty("api", context.Api.Name),
            new JProperty("operation", context.Operation.Name),
            new JProperty("method", context.Request.Method),
            new JProperty("url", context.Request.Url.ToString()),
            new JProperty("subscriptionId", context.Subscription?.Id ?? "anonymous"),
            new JProperty("ip", context.Request.IpAddress),
            new JProperty("timestamp", DateTime.UtcNow.ToString("o"))
        ).ToString();
    }</set-body>
</send-request>
<!-- auditIgnored is never read — we don't care about the response -->

4. Runtime Feature Flag Check

Call a feature flag service to decide policy behaviour:

<send-request mode="new" response-variable-name="featureFlags" timeout="3" ignore-error="true">
    <set-url>@("https://flags.internal/check?feature=new-pricing&env=" + "{{EnvironmentName}}")</set-url>
    <set-method>GET</set-method>
    <set-header name="X-Api-Key" exists-action="override">
        <value>{{FeatureFlagApiKey}}</value>
    </set-header>
</send-request>

<set-variable name="useNewPricing" value="@{
    try {
        var resp = (IResponse)context.Variables["featureFlags"];
        if (resp?.StatusCode == 200) {
            return resp.Body.As<JObject>()["enabled"]?.Value<bool>() ?? false;
        }
    } catch {}
    return false;
}" />

5. Webhook Notification on Response

Trigger a webhook after the backend responds successfully:

<!-- In outbound section — only fires on 2xx -->
<choose>
    <when condition="@(context.Response.StatusCode >= 200 && context.Response.StatusCode < 300)">
        <send-request mode="new" response-variable-name="webhookResult" timeout="3" ignore-error="true">
            <set-url>{{WebhookUrl}}</set-url>
            <set-method>POST</set-method>
            <set-header name="Content-Type" exists-action="override">
                <value>application/json</value>
            </set-header>
            <set-header name="X-Webhook-Secret" exists-action="override">
                <value>{{WebhookSecret}}</value>
            </set-header>
            <set-body>@{
                return new JObject(
                    new JProperty("event", "request-succeeded"),
                    new JProperty("statusCode", context.Response.StatusCode),
                    new JProperty("operation", context.Operation.Name),
                    new JProperty("timestamp", DateTime.UtcNow.ToString("o"))
                ).ToString();
            }</set-body>
        </send-request>
    </when>
</choose>

Solving the Tracking Issue End-to-End

Here is a complete, production-ready pattern that solves every tracking issue — correlation ID, context isolation, and clean audit logging — all in one policy.

Step 1: Inject Correlation ID First (Before Any send-request)

When a third-party caller sends their own Request-Id, you capture it separately and link it — never use it as your internal correlation ID, and never let it bleed onto your internal service headers.

<!-- Always do this FIRST in inbound, before any send-request calls -->

<!-- Capture third-party's Request-Id if present (check both common header names) -->
<set-variable name="thirdPartyRequestId" value="@{
    var tpId = context.Request.Headers.GetValueOrDefault("Request-Id", "");
    if (string.IsNullOrEmpty(tpId)) {
        tpId = context.Request.Headers.GetValueOrDefault("X-Request-Id", "");
    }
    return tpId;  // Empty string if not a third-party call — that's fine
}" />

<!-- Always generate YOUR OWN internal correlation ID -->
<!-- If the caller sent Request-Id (a trusted internal caller), honour it;
     otherwise generate a fresh one — never blindly trust a third-party ID -->
<set-variable name="requestId" value="@{
    var existing = context.Request.Headers.GetValueOrDefault("Request-Id", "");
    return string.IsNullOrEmpty(existing) ? Guid.NewGuid().ToString() : existing;
}" />

<!-- Stamp your correlation ID on the request and response — not the third-party's -->
<set-header name="Request-Id" exists-action="override">
    <value>@(context.Variables.GetValueOrDefault<string>("requestId"))</value>
</set-header>

Step 2: Use mode="new" With Explicit Correlation in Every send-request

<!-- Every internal send-request carries YOUR correlation ID, not the client's -->
<send-request mode="new" response-variable-name="enrichmentData" timeout="5">
    <set-url>https://enrichment.internal/data</set-url>
    <set-method>GET</set-method>
    <set-header name="X-Api-Key" exists-action="override">
        <value>{{EnrichmentApiKey}}</value>
    </set-header>
    <!-- Explicitly pass YOUR correlation ID — not copied from client -->
    <set-header name="Request-Id" exists-action="override">
        <value>@(context.Variables.GetValueOrDefault<string>("requestId"))</value>
    </set-header>
    <!-- No Authorization forwarded — internal service uses its own API key -->
</send-request>

Step 3: Propagate Correlation ID to Backend

<!-- Before base — make sure the backend also gets your correlation ID -->
<set-header name="Request-Id" exists-action="override">
    <value>@(context.Variables.GetValueOrDefault<string>("requestId"))</value>
</set-header>
<base />

Step 4: Fire-and-Forget Audit With Full Trace

<send-request mode="new" response-variable-name="audit" timeout="2" ignore-error="true">
    <set-url>https://audit.internal/events</set-url>
    <set-method>POST</set-method>
    <set-header name="Content-Type" exists-action="override">
        <value>application/json</value>
    </set-header>
    <set-header name="X-Api-Key" exists-action="override">
        <value>{{AuditApiKey}}</value>
    </set-header>
    <set-header name="Request-Id" exists-action="override">
        <value>@(context.Variables.GetValueOrDefault<string>("requestId"))</value>
    </set-header>
    <set-body>@{
        return new JObject(
            new JProperty("requestId", context.Variables.GetValueOrDefault<string>("requestId")),
            new JProperty("thirdPartyRequestId", context.Variables.GetValueOrDefault<string>("thirdPartyRequestId", "")),
            new JProperty("api", context.Api.Name),
            new JProperty("operation", context.Operation.Name),
            new JProperty("subscriptionId", context.Subscription?.Id ?? "anonymous"),
            new JProperty("clientIp", context.Request.IpAddress),
            new JProperty("method", context.Request.Method),
            new JProperty("timestamp", DateTime.UtcNow.ToString("o"))
        ).ToString();
    }</set-body>
</send-request>

Complete Policy Putting It All Together

<policies>
    <inbound>
        <!-- Step 1: Capture third-party Request-Id and establish internal correlation ID -->
        <set-variable name="thirdPartyRequestId" value="@{
            var tpId = context.Request.Headers.GetValueOrDefault("Request-Id", "");
            if (string.IsNullOrEmpty(tpId)) {
                tpId = context.Request.Headers.GetValueOrDefault("X-Request-Id", "");
            }
            return tpId;
        }" />
        <set-variable name="requestId" value="@{
            var existing = context.Request.Headers.GetValueOrDefault("Request-Id", "");
            return string.IsNullOrEmpty(existing) ? Guid.NewGuid().ToString() : existing;
        }" />
        <set-header name="Request-Id" exists-action="override">
            <value>@(context.Variables.GetValueOrDefault<string>("requestId"))</value>
        </set-header>

        <!-- Step 2: Enrich with internal data — mode="new", no leakage -->
        <send-request mode="new" response-variable-name="userProfile" timeout="5" ignore-error="true">
            <set-url>@("https://users.internal/profile/" + context.Subscription.Id)</set-url>
            <set-method>GET</set-method>
            <set-header name="X-Api-Key" exists-action="override">
                <value>{{UserServiceApiKey}}</value>
            </set-header>
            <set-header name="Request-Id" exists-action="override">
                <value>@(context.Variables.GetValueOrDefault<string>("requestId"))</value>
            </set-header>
        </send-request>

        <!-- Step 3: Inject enriched data into backend request -->
        <set-variable name="userTier" value="@{
            try {
                var resp = (IResponse)context.Variables["userProfile"];
                if (resp?.StatusCode == 200) {
                    return resp.Body.As<JObject>()["tier"]?.ToString() ?? "free";
                }
            } catch {}
            return "free";
        }" />
        <set-header name="X-User-Tier" exists-action="override">
            <value>@(context.Variables.GetValueOrDefault<string>("userTier"))</value>
        </set-header>

        <!-- Step 4: Audit log — fire-and-forget, mode="new" -->
        <send-request mode="new" response-variable-name="auditIgnored" timeout="2" ignore-error="true">
            <set-url>https://audit.internal/events</set-url>
            <set-method>POST</set-method>
            <set-header name="Content-Type" exists-action="override">
                <value>application/json</value>
            </set-header>
            <set-header name="X-Api-Key" exists-action="override">
                <value>{{AuditApiKey}}</value>
            </set-header>
            <set-header name="Request-Id" exists-action="override">
                <value>@(context.Variables.GetValueOrDefault<string>("requestId"))</value>
            </set-header>
            <set-body>@{
                return new JObject(
                    new JProperty("requestId", context.Variables.GetValueOrDefault<string>("requestId")),
                    new JProperty("thirdPartyRequestId", context.Variables.GetValueOrDefault<string>("thirdPartyRequestId", "")),
                    new JProperty("event", "inbound"),
                    new JProperty("api", context.Api.Name),
                    new JProperty("operation", context.Operation.Name),
                    new JProperty("subscriptionId", context.Subscription?.Id ?? "anonymous"),
                    new JProperty("userTier", context.Variables.GetValueOrDefault<string>("userTier")),
                    new JProperty("clientIp", context.Request.IpAddress),
                    new JProperty("timestamp", DateTime.UtcNow.ToString("o"))
                ).ToString();
            }</set-body>
        </send-request>

        <!-- Step 5: Forward correlation ID to backend -->
        <set-header name="Request-Id" exists-action="override">
            <value>@(context.Variables.GetValueOrDefault<string>("requestId"))</value>
        </set-header>

        <base />
    </inbound>

    <backend>
        <base />
    </backend>

    <outbound>
        <!-- Ensure correlation ID is in the response too -->
        <set-header name="Request-Id" exists-action="override">
            <value>@(context.Variables.GetValueOrDefault<string>("requestId"))</value>
        </set-header>
        <base />
    </outbound>

    <on-error>
        <set-header name="Request-Id" exists-action="override">
            <value>@(context.Variables.GetValueOrDefault<string>("requestId"))</value>
        </set-header>
        <base />
    </on-error>
</policies>

Common Mistakes and How to Fix Them

Mistake 1: Using mode="copy" as the Default

<!-- ❌ Wrong — copy is not a safe default -->
<send-request mode="copy" response-variable-name="result" timeout="5">
    <set-url>https://internal-service.com/api</set-url>
    <set-method>POST</set-method>
    <set-body>{"myPayload": true}</set-body>
    <!-- Body is set, but ALL original headers still leak through -->
</send-request>

<!-- ✅ Correct — new is the safe default for all internal calls -->
<send-request mode="new" response-variable-name="result" timeout="5">
    <set-url>https://internal-service.com/api</set-url>
    <set-method>POST</set-method>
    <set-header name="Content-Type" exists-action="override">
        <value>application/json</value>
    </set-header>
    <set-body>{"myPayload": true}</set-body>
</send-request>

Mistake 2: Forgetting Content-Type With mode="new"

With mode="copy", Content-Type is inherited. With mode="new" it is not — and if your body is JSON but Content-Type is missing, the downstream service may return 415 Unsupported Media Type.

<!-- ❌ Missing Content-Type — downstream may reject with 415 -->
<send-request mode="new" response-variable-name="result" timeout="5">
    <set-url>https://service.internal/api</set-url>
    <set-method>POST</set-method>
    <set-body>{"data": "value"}</set-body>
</send-request>

<!-- ✅ Always explicitly set Content-Type with mode="new" -->
<send-request mode="new" response-variable-name="result" timeout="5">
    <set-url>https://service.internal/api</set-url>
    <set-method>POST</set-method>
    <set-header name="Content-Type" exists-action="override">
        <value>application/json</value>
    </set-header>
    <set-body>{"data": "value"}</set-body>
</send-request>

Mistake 3: Not Handling ignore-error Correctly

For non-critical calls (audit, logging), always set ignore-error="true". For critical calls (token fetch, auth validation), leave it as false — you want the pipeline to fail if those calls fail.

<!-- Critical path — failure must stop the request -->
<send-request mode="new" response-variable-name="authToken" timeout="10" ignore-error="false">
    <set-url>https://auth.internal/token</set-url>
    ...
</send-request>

<!-- Non-critical path — failure must NOT stop the request -->
<send-request mode="new" response-variable-name="auditIgnored" timeout="2" ignore-error="true">
    <set-url>https://audit.internal/log</set-url>
    ...
</send-request>

Mistake 4: Setting Timeout Too High

Every send-request adds to the client's total wait time. Keep timeouts tight:

Call TypeRecommended Timeout
Audit / logging (fire-and-forget)1–2 seconds
Token fetch (cached or fast IdP)5 seconds
Data enrichment (non-critical)3–5 seconds
Auth validation (critical path)5–10 seconds
Shadow traffic3 seconds

Mistake 5: Not Casting the Response Variable

The response variable is typed as object in context. You must cast it to IResponse before reading:

<!-- ❌ Wrong — will throw at runtime -->
<set-variable name="status" value="@(context.Variables["myResponse"].StatusCode)" />

<!-- ✅ Correct — cast to IResponse first -->
<set-variable name="status" value="@(((IResponse)context.Variables["myResponse"]).StatusCode)" />

<!-- ✅ Also correct — safer with null check -->
<set-variable name="isOk" value="@{
    var resp = context.Variables["myResponse"] as IResponse;
    return resp != null && resp.StatusCode == 200;
}" />

Quick Reference

Attributes of send-request

AttributeRequiredDescription
modeYesnew (blank request) or copy (clone of original)
response-variable-nameYesName of the context variable to store the response
timeoutNoSeconds before the call times out (default: 60)
ignore-errorNotrue = swallow errors silently; false = fail the pipeline (default: false)

Child Elements

ElementDescription
<set-url>The target URL — supports policy expressions
<set-method>HTTP method (GET, POST, PUT, DELETE, etc.)
<set-header>Add or override a header
<set-body>Set the request body — supports policy expressions

Reading the Response

<!-- Status code -->
@(((IResponse)context.Variables["myVar"]).StatusCode)

<!-- Response header -->
@(((IResponse)context.Variables["myVar"]).Headers.GetValueOrDefault("X-Header", ""))

<!-- Response body as string -->
@(((IResponse)context.Variables["myVar"]).Body.As<string>())

<!-- Response body as JObject (JSON) -->
@(((IResponse)context.Variables["myVar"]).Body.As<JObject>())

Summary

The send-request policy is one of the most versatile tools in APIM — but mode="copy" silently causes context leakage, header pollution, and tracking corruption that is hard to diagnose.

The rules to remember:

When these rules are followed, send-request becomes a clean, observable, and secure mechanism for everything from audit logging to service orchestration inside your APIM gateway.