Azure API Management — APIM → Service Bus Integration

send-request Policy to Publish Messages, 202 Pattern


Introduction

Integrating Azure API Management with Azure Service Bus enables powerful asynchronous processing patterns. Instead of blocking HTTP requests while heavy processing completes, you can queue messages to Service Bus and return immediately with a 202 Accepted response.

This pattern is ideal for:

  • Long-running operations — Process orders, generate reports, etc.
  • High-throughput scenarios — Offload processing to background workers
  • Resilient workflows — Messages persist until processed
  • Decoupled architectures — Producer and consumer separated

Architecture Pattern

┌──────────┐    ┌────────────┐    ┌───────────────┐    ┌─────────────────┐
│  Client  │───▶│    APIM    │───▶│ Service Bus   │───▶│  Worker/Func    │
│          │    │  (Policy)  │    │   Queue       │    │  (Processor)    │
└──────────┘    └────────────┘    └───────────────┘    └─────────────────┘
     │                                 │
     │ 202 Accepted                    │
     │ {                               │
     │   "status": "processing",       │
     │   "trackingId": "xxx"           │
     │ }                               │
     ▼                                 │
┌──────────┐                           │
│ Poll     │◀──────────────────────────┘
│ Status   │        GET /status/{id}
└──────────┘

Step 1: Configure Managed Identity

Enable MI in APIM

# Enable system-assigned managed identity
az apim identity assign \
  --name my-apim \
  --resource-group my-rg

# Or use user-assigned identity
az apim identity assign \
  --name my-apim \
  --resource-group my-rg \
  --user-assigned /subscriptions/xxx/resourceGroups/my-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/my-identity

Grant Service Bus Permissions

# Get APIM's identity object ID
APIM_ID=$(az apim show --name my-apim --resource-group my-rg --query identity.principalId -o tsv)

# Grant Service Bus Sender role
az role assignment create \
  --assignee $APIM_ID \
  --scope /subscriptions/xxx/resourceGroups/my-rg/providers/Microsoft.ServiceBus/namespaces/my-namespace \
  --role "Azure Service Bus Data Sender"

Step 2: Create APIM Policy

Send to Service Bus

<policies>
    <inbound>
        <base />
        
        <!-- Set backend to a placeholder - we'll use send-request -->
        <set-backend-service backend-id="placeholder" />
    </inbound>
    
    <backend>
        <!-- Send to Service Bus Queue -->
        <send-request mode="new" timeout="30" response-variable-name="sb-response" ignore-error="false">
            <set-url>@($"https://my-namespace.servicebus.windows.net/orders/messages")</set-url>
            <set-method>POST</set-method>
            <set-header name="Authorization" exists-action="override">
                <value>@{
                    // Get token using Managed Identity
                    var tokenEndpoint = "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2019-08-01&resource=https://servicebus.azure.net/";
                    var tokenRequest = new HttpRequestMessage(HttpMethod.Get, tokenEndpoint);
                    tokenRequest.Headers.Add("Metadata", "true");
                    
                    using var tokenClient = new HttpClient();
                    var tokenResponse = tokenClient.Send(tokenRequest);
                    var tokenJson = await tokenResponse.Content.ReadAsStringAsync();
                    var token = System.Text.Json.JsonSerializer.Deserialize<TokenResponse>(tokenJson);
                    
                    return $"Bearer {token.access_token}";
                }</value>
            </set-header>
            <set-header name="Content-Type" exists-action="override">application/json</set-header>
            <set-body>@{
                var requestBody = context.Request.Body.As<JsonElement>();
                var trackingId = Guid.NewGuid().ToString();
                
                // Store tracking ID for later retrieval
                context.Variables["trackingId"] = trackingId;
                
                return new {
                    trackingId = trackingId,
                    orderId = requestBody.GetProperty("orderId").GetString(),
                    customerId = requestBody.GetProperty("customerId").GetString(),
                    items = requestBody.GetProperty("items"),
                    submittedAt = DateTime.UtcNow.ToString("O")
                };
            }</set-body>
        </send-request>
    </backend>
    
    <outbound>
        <base />
        <return-response>
            <set-status code="202" reason="Accepted" />
            <set-body>@{
                return new {
                    status = "Accepted",
                    trackingId = context.Variables["trackingId"],
                    message = "Your request is being processed",
                    statusUrl = $"https://api.example.com/orders/{context.Variables["trackingId"]}/status"
                };
            }</set-body>
        </return-response>
    </outbound>
</policies>

Step 3: Using Named Values

Create SAS-Based Authentication

<!-- Store connection info in Named Values -->
<!-- ServiceBusSharedAccessKey: Endpoint=sb://my-namespace.servicebus.windows.net/;SharedAccessKeyName=xxx;SharedAccessKey=xxx -->

<policies>
    <backend>
        <send-request mode="new" timeout="30" response-variable-name="sb-response">
            <set-url>@($"https://my-namespace.servicebus.windows.net/orders/messages")</set-url>
            <set-method>POST</set-method>
            <set-header name="Authorization" exists-action="override">
                <value>@{
                    var key = "Endpoint=sb://my-namespace.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey={{ServiceBusKey}}";
                    var parts = key.Split(';');
                    var resource = "https://servicebus.azure.net/";
                    var expiry = DateTimeOffset.UtcNow.AddHours(1).ToUnixTimeSeconds();
                    
                    var stringToSign = Uri.EscapeDataString($"https://my-namespace.servicebus.windows.net/orders/messages\n{expiry}");
                    var signature = ComputeHMACSHA256(stringToSign, parts[2].Split('=')[1]);
                    
                    return $"SharedAccessSignature sr=https%3A%2F%2Fmy-namespace.servicebus.windows.net%2Forders&sig={Uri.EscapeDataString(signature)}&se={expiry}&skn={parts[1].Split('=')[1]}";
                }</value>
            </set-header>
            <set-header name="Content-Type" exists-action="override">application/json</set-header>
            <set-body>@{
                return context.Request.Body.As<JsonElement>();
            }</set-body>
        </send-request>
    </backend>
</policies>

Step 4: 202 Accepted Pattern

Complete Workflow

<policies>
    <inbound>
        <base />
        
        <!-- Validate request -->
        <validate-jwt header-name="Authorization">
            <openid-config url="https://login.microsoftonline.com/tenant/v2.0/.well-known/openid-configuration" />
            <audiences>
                <audience>api://my-api</audience>
            </audiences>
        </validate-jwt>
        
        <!-- Rate limiting -->
        <rate-limit-by-key calls="100" renewal-period="60" 
            counter-key="@(context.Subscription.Id)" />
    </inbound>
    
    <backend>
        <!-- Send to Service Bus -->
        <send-request mode="new" timeout="30" response-variable-name="sb-response">
            <set-url>@($"https://my-namespace.servicebus.windows.net/orders/messages")</set-url>
            <set-method>POST</set-method>
            <authentication-managed-identity resource="https://servicebus.azure.net/" />
            <set-header name="Content-Type" exists-action="override">application/json</set-header>
            <set-body>@{
                var body = context.Request.Body.As<JsonElement>();
                var trackingId = Guid.NewGuid().ToString();
                
                context.Variables["trackingId"] = trackingId;
                context.Variables["receivedAt"] = DateTime.UtcNow;
                
                return new {
                    trackingId = trackingId,
                    data = body,
                    metadata = new {
                        apiName = context.Api.Name,
                        operationName = context.Operation.Name,
                        subscriptionId = context.Subscription.Id,
                        receivedAt = DateTime.UtcNow
                    }
                };
            }</set-body>
        </send-request>
    </backend>
    
    <outbound>
        <base />
        
        <!-- Return 202 Accepted -->
        <return-response>
            <set-status code="202" reason="Accepted" />
            <set-header name="Location" exists-action="override">
                <value>@{
                    var trackingId = context.Variables["trackingId"];
                    return $"/orders/{trackingId}/status";
                }</value>
            </set-header>
            <set-header name="Retry-After" exists-action="override">
                <value>30</value>
            </set-header>
            <set-body>@{
                return new {
                    status = "processing",
                    trackingId = context.Variables["trackingId"],
                    estimatedCompletion = DateTime.UtcNow.AddMinutes(5),
                    statusUrl = $"/orders/{context.Variables["trackingId"]}/status"
                };
            }</set-body>
        </return-response>
    </outbound>
</policies>

Step 5: Status Check Endpoint

Status Query API

<policies>
    <inbound>
        <!-- Extract tracking ID from URL -->
        <rewrite-uri template="/orders/status" />
    </inbound>
    
    <backend>
        <!-- Query Redis/CosmosDB for status -->
        <send-request mode="new" timeout="10" response-variable-name="status-response">
            <set-url>@{
                var trackingId = context.Request.MatchedParameters["trackingId"];
                return $"https://my-redis.redis.azure.com/status/{trackingId}";
            }</set-url>
            <set-method>GET</set-method>
            <authentication-managed-identity resource="https://redis.azure.net/" />
        </send-request>
    </backend>
    
    <outbound>
        <base />
        <return-response>
            <set-body>@{
                var status = ((IResponse)context.Variables["status-response"]).Body.As<JsonElement>();
                return new {
                    trackingId = status.GetProperty("trackingId"),
                    status = status.GetProperty("status"),
                    progress = status.GetProperty("progress"),
                    result = status.GetProperty("result"),
                    completedAt = status.TryGetProperty("completedAt", out var completed) ? completed : null
                };
            }</set-body>
        </return-response>
    </outbound>
</policies>

Step 6: Worker Function

Processing Messages

[FunctionName("OrderProcessor")]
public static async Task Run(
    [ServiceBusTrigger("orders", Connection = "ServiceBusConnection")] 
    ServiceBusReceivedMessage message,
    ServiceBusClient client,
    ILogger logger)
{
    var body = JsonSerializer.Deserialize<OrderMessage>(
        Encoding.UTF8.GetString(message.Body));
    
    logger.LogInformation("Processing order {TrackingId}", body.TrackingId);
    
    try
    {
        // Update status to processing
        await UpdateStatusAsync(body.TrackingId, "processing", 0);
        
        // Process the order
        var result = await ProcessOrderAsync(body.Data);
        
        // Update status to completed
        await UpdateStatusAsync(body.TrackingId, "completed", 100, result);
        
        logger.LogInformation("Order {TrackingId} completed", body.TrackingId);
    }
    catch (Exception ex)
    {
        logger.LogError(ex, "Failed to process order {TrackingId}", body.TrackingId);
        await UpdateStatusAsync(body.TrackingId, "failed", 0, new { error = ex.Message });
        
        if (ex is RetryableException)
        {
            throw; // Will be retried
        }
    }
}

private async Task UpdateStatusAsync(
    string trackingId, 
    string status, 
    int progress, 
    object result = null)
{
    var redis = _redisClient.GetDatabase();
    var key = $"order:status:{trackingId}";
    
    var statusObj = new
    {
        trackingId,
        status,
        progress,
        result,
        updatedAt = DateTime.UtcNow
    };
    
    await redis.StringSetAsync(key, JsonSerializer.Serialize(statusObj));
    await redis.KeyExpireAsync(key, TimeSpan.FromDays(7));
}

Error Handling

Circuit Breaker Pattern

<backend>
    <choose>
        <when condition="@(context.Variables.GetValue<bool>("circuitOpen"))">
            <return-response>
                <set-status code="503" />
                <set-body>Service temporarily unavailable</set-body>
            </return-response>
        </when>
    </choose>
    
    <send-request mode="new" timeout="30" response-variable-name="sb-response">
        <!-- ... -->
    </send-request>
    
    <choose>
        <when condition="@(((IResponse)context.Variables["sb-response"]).StatusCode >= 500)">
            <set-variable name="failureCount" 
                value="@(context.Variables.GetValue<int>("failureCount") + 1)" />
            
            <choose>
                <when condition="@(context.Variables.GetValue<int>("failureCount") >= 5)">
                    <set-variable name="circuitOpen" value="true" />
                    <set-variable name="circuitOpenedAt" value="@(DateTime.UtcNow)" />
                </when>
            </choose>
        </when>
    </choose>
</backend>

Benefits

BenefitDescription
Non-blockingClient receives response immediately
ReliableMessages persist until processed
ScalableWorker scales independently
MonitorableTrack processing status
ResilientFailed requests can retry

Azure Integration Hub - Advanced Level